ensure-app-member-schemas.mjs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #!/usr/bin/env node
  2. const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
  3. const MASTER_KEY = process.env.XIAOSHU_MASTER_KEY || '';
  4. const PARSE_URL = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, '');
  5. if (!MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY');
  6. const headers = {
  7. 'X-Parse-Application-Id': APP_ID,
  8. 'X-Parse-Master-Key': MASTER_KEY,
  9. 'Content-Type': 'application/json',
  10. };
  11. const pointer = (targetClass) => ({ type: 'Pointer', targetClass });
  12. const lockedPermissions = {
  13. find: {}, count: {}, get: {}, create: {}, update: {}, delete: {}, addField: {},
  14. };
  15. const shared = {
  16. company: pointer('Company'),
  17. user: pointer('_User'),
  18. legacyUserId: { type: 'Number' },
  19. isDeleted: { type: 'Boolean' },
  20. sourceKey: { type: 'String' },
  21. };
  22. const schemas = {
  23. ReceivingAddress: {
  24. ...shared,
  25. receiverName: { type: 'String' }, mobile: { type: 'String' },
  26. provinceName: { type: 'String' }, cityName: { type: 'String' }, countyName: { type: 'String' },
  27. detailInfo: { type: 'String' }, postalCode: { type: 'String' }, isDefault: { type: 'Boolean' },
  28. },
  29. InvoiceProfile: {
  30. ...shared,
  31. invoiceType: { type: 'Number' }, title: { type: 'String' }, taxId: { type: 'String' },
  32. mobile: { type: 'String' }, email: { type: 'String' }, address: { type: 'String' },
  33. bankName: { type: 'String' }, bankAccount: { type: 'String' },
  34. },
  35. UserBankAccount: {
  36. ...shared,
  37. cardType: { type: 'String' }, bankName: { type: 'String' }, accountName: { type: 'String' },
  38. accountNumber: { type: 'String' }, remark: { type: 'String' },
  39. },
  40. UserFavorite: {
  41. ...shared,
  42. targetId: { type: 'String' }, targetType: { type: 'Number' }, title: { type: 'String' }, url: { type: 'String' },
  43. },
  44. UnitLearningRecord: {
  45. ...shared,
  46. bookId: { type: 'Number' }, nodeId: { type: 'Number' }, wordIds: { type: 'Array' }, learnedCount: { type: 'Number' },
  47. },
  48. ServiceRequest: {
  49. ...shared,
  50. pubId: { type: 'Number' }, category: { type: 'String' }, title: { type: 'String' },
  51. content: { type: 'String' }, contact: { type: 'String' }, payload: { type: 'Object' },
  52. status: { type: 'String' }, reviewedAt: { type: 'Date' }, handledBy: pointer('_User'),
  53. },
  54. CommerceProduct: {
  55. company: pointer('Company'), owner: pointer('_User'), legacyOwnerId: { type: 'Number' },
  56. productId: { type: 'Number' }, name: { type: 'String' }, nodeId: { type: 'Number' },
  57. retailPrice: { type: 'Number' }, marketPrice: { type: 'Number' }, unit: { type: 'String' },
  58. stock: { type: 'Number' }, thumbnail: { type: 'String' }, gallery: { type: 'Array' },
  59. summary: { type: 'String' }, content: { type: 'String' }, status: { type: 'String' },
  60. sales: { type: 'Number' }, hot: { type: 'Boolean' }, isDeleted: { type: 'Boolean' }, sourceKey: { type: 'String' },
  61. },
  62. ShoppingCartItem: {
  63. ...shared,
  64. product: pointer('CommerceProduct'), productId: { type: 'Number' }, quantity: { type: 'Number' }, selected: { type: 'Boolean' },
  65. },
  66. ProductStockLedger: {
  67. company: pointer('Company'), user: pointer('_User'), legacyUserId: { type: 'Number' },
  68. product: pointer('CommerceProduct'), productId: { type: 'Number' }, delta: { type: 'Number' },
  69. stockBefore: { type: 'Number' }, stockAfter: { type: 'Number' }, remark: { type: 'String' }, sourceKey: { type: 'String' },
  70. },
  71. ProductComment: {
  72. ...shared,
  73. product: pointer('CommerceProduct'), productId: { type: 'Number' }, orderId: { type: 'String' },
  74. title: { type: 'String' }, score: { type: 'Number' }, content: { type: 'String' }, images: { type: 'Array' },
  75. anonymous: { type: 'Boolean' }, status: { type: 'String' },
  76. },
  77. CommerceOrder: {
  78. company: pointer('Company'), user: pointer('_User'), buyerLegacyId: { type: 'Number' },
  79. orderId: { type: 'Number' }, orderNo: { type: 'String' }, status: { type: 'String' },
  80. subtotal: { type: 'Number' }, discount: { type: 'Number' }, shipping: { type: 'Number' }, total: { type: 'Number' },
  81. receiver: { type: 'Object' }, invoice: { type: 'Object' }, couponId: { type: 'String' }, paymentMode: { type: 'String' },
  82. paidAt: { type: 'Date' }, shippedAt: { type: 'Date' }, signedAt: { type: 'Date' },
  83. expressCompany: { type: 'String' }, expressNo: { type: 'String' }, expressRemark: { type: 'String' },
  84. isDeleted: { type: 'Boolean' }, sourceKey: { type: 'String' },
  85. },
  86. CommerceOrderItem: {
  87. company: pointer('Company'), order: pointer('CommerceOrder'), orderId: { type: 'Number' },
  88. product: pointer('CommerceProduct'), productId: { type: 'Number' }, seller: pointer('_User'), sellerLegacyId: { type: 'Number' },
  89. name: { type: 'String' }, unitPrice: { type: 'Number' }, quantity: { type: 'Number' }, amount: { type: 'Number' },
  90. thumbnail: { type: 'String' }, productSnapshot: { type: 'Object' }, sourceKey: { type: 'String' },
  91. },
  92. CommercePayment: {
  93. company: pointer('Company'), user: pointer('_User'), legacyUserId: { type: 'Number' },
  94. order: pointer('CommerceOrder'), orderId: { type: 'Number' }, amount: { type: 'Number' }, mode: { type: 'String' },
  95. status: { type: 'String' }, transactionNo: { type: 'String' }, paidAt: { type: 'Date' }, sourceKey: { type: 'String' },
  96. },
  97. CouponDefinition: {
  98. company: pointer('Company'), couponId: { type: 'Number' }, name: { type: 'String' }, amount: { type: 'Number' },
  99. minAmount: { type: 'Number' }, maxAmount: { type: 'Number' }, startsAt: { type: 'Date' }, expiresAt: { type: 'Date' },
  100. status: { type: 'String' }, totalQuantity: { type: 'Number' }, claimedQuantity: { type: 'Number' }, sourceKey: { type: 'String' },
  101. },
  102. UserCoupon: {
  103. ...shared,
  104. coupon: pointer('CouponDefinition'), couponId: { type: 'Number' }, status: { type: 'String' },
  105. usedAt: { type: 'Date' }, expiresAt: { type: 'Date' },
  106. },
  107. MembershipStockLedger: {
  108. company: pointer('Company'), operator: pointer('_User'), operatorLegacyId: { type: 'Number' },
  109. target: pointer('_User'), targetLegacyId: { type: 'Number' }, type: { type: 'String' },
  110. groupBefore: { type: 'Number' }, groupAfter: { type: 'Number' }, quantity: { type: 'Number' },
  111. operatorStockBefore: { type: 'Number' }, operatorStockAfter: { type: 'Number' },
  112. targetStockBefore: { type: 'Number' }, targetStockAfter: { type: 'Number' },
  113. remark: { type: 'String' }, sourceKey: { type: 'String' },
  114. },
  115. AppAuditLog: {
  116. company: pointer('Company'), user: pointer('_User'), legacyUserId: { type: 'Number' },
  117. action: { type: 'String' }, targetClass: { type: 'String' }, targetId: { type: 'String' },
  118. before: { type: 'Object' }, after: { type: 'Object' }, ip: { type: 'String' }, sourceKey: { type: 'String' },
  119. },
  120. LegacySyncCheckpoint: {
  121. company: pointer('Company'), sourceKey: { type: 'String' }, dataset: { type: 'String' },
  122. cursor: { type: 'String' }, state: { type: 'String' }, processedCount: { type: 'Number' },
  123. lastBatchCount: { type: 'Number' }, lastFailureCount: { type: 'Number' },
  124. lastAttemptAt: { type: 'Date' }, lastSuccessAt: { type: 'Date' },
  125. leaseToken: { type: 'String' }, leaseUntil: { type: 'Date' },
  126. lastServerTime: { type: 'Date' }, lastWatermark: { type: 'Number' },
  127. },
  128. LegacySyncConflict: {
  129. company: pointer('Company'), sourceKey: { type: 'String' }, dataset: { type: 'String' },
  130. legacyKey: { type: 'String' }, conflictType: { type: 'String' }, message: { type: 'String' },
  131. resolved: { type: 'Boolean' }, resolvedAt: { type: 'Date' }, resolution: { type: 'String' },
  132. },
  133. LegacySyncDeadLetter: {
  134. company: pointer('Company'), sourceKey: { type: 'String' }, dataset: { type: 'String' },
  135. legacyKey: { type: 'String' }, recordClasses: { type: 'Array' }, errorCode: { type: 'Number' },
  136. errorMessage: { type: 'String' }, attempts: { type: 'Number' }, resolved: { type: 'Boolean' },
  137. lastAttemptAt: { type: 'Date' }, resolvedAt: { type: 'Date' },
  138. },
  139. };
  140. async function request(path, init = {}) {
  141. const response = await fetch(`${PARSE_URL}${path}`, { ...init, headers: { ...headers, ...(init.headers || {}) } });
  142. const payload = await response.json().catch(() => ({}));
  143. if (!response.ok || payload.error) throw new Error(`${path}: ${payload.error || response.status}`);
  144. return payload;
  145. }
  146. for (const [className, fields] of Object.entries(schemas)) {
  147. const existingResponse = await fetch(`${PARSE_URL}/schemas/${className}`, { headers });
  148. const existing = await existingResponse.json().catch(() => ({}));
  149. if (existingResponse.status === 404 || (!existingResponse.ok && /does not exist/i.test(String(existing.error || '')))) {
  150. await request('/schemas/' + className, {
  151. method: 'POST',
  152. body: JSON.stringify({ className, fields, classLevelPermissions: lockedPermissions }),
  153. });
  154. console.log(`created ${className}`);
  155. continue;
  156. }
  157. if (!existingResponse.ok || existing.error) throw new Error(`${className}: ${existing.error || existingResponse.status}`);
  158. const additions = Object.fromEntries(Object.entries(fields).filter(([name]) => !existing.fields?.[name]));
  159. if (Object.keys(additions).length) {
  160. await request('/schemas/' + className, { method: 'PUT', body: JSON.stringify({ fields: additions }) });
  161. console.log(`updated ${className}: ${Object.keys(additions).join(', ')}`);
  162. } else console.log(`current ${className}`);
  163. }