| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- #!/usr/bin/env node
- const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
- const MASTER_KEY = process.env.XIAOSHU_MASTER_KEY || '';
- const PARSE_URL = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, '');
- if (!MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY');
- const headers = {
- 'X-Parse-Application-Id': APP_ID,
- 'X-Parse-Master-Key': MASTER_KEY,
- 'Content-Type': 'application/json',
- };
- const pointer = (targetClass) => ({ type: 'Pointer', targetClass });
- const lockedPermissions = {
- find: {}, count: {}, get: {}, create: {}, update: {}, delete: {}, addField: {},
- };
- const shared = {
- company: pointer('Company'),
- user: pointer('_User'),
- legacyUserId: { type: 'Number' },
- isDeleted: { type: 'Boolean' },
- sourceKey: { type: 'String' },
- };
- const schemas = {
- ReceivingAddress: {
- ...shared,
- receiverName: { type: 'String' }, mobile: { type: 'String' },
- provinceName: { type: 'String' }, cityName: { type: 'String' }, countyName: { type: 'String' },
- detailInfo: { type: 'String' }, postalCode: { type: 'String' }, isDefault: { type: 'Boolean' },
- },
- InvoiceProfile: {
- ...shared,
- invoiceType: { type: 'Number' }, title: { type: 'String' }, taxId: { type: 'String' },
- mobile: { type: 'String' }, email: { type: 'String' }, address: { type: 'String' },
- bankName: { type: 'String' }, bankAccount: { type: 'String' },
- },
- UserBankAccount: {
- ...shared,
- cardType: { type: 'String' }, bankName: { type: 'String' }, accountName: { type: 'String' },
- accountNumber: { type: 'String' }, remark: { type: 'String' },
- },
- UserFavorite: {
- ...shared,
- targetId: { type: 'String' }, targetType: { type: 'Number' }, title: { type: 'String' }, url: { type: 'String' },
- },
- UnitLearningRecord: {
- ...shared,
- bookId: { type: 'Number' }, nodeId: { type: 'Number' }, wordIds: { type: 'Array' }, learnedCount: { type: 'Number' },
- },
- ServiceRequest: {
- ...shared,
- pubId: { type: 'Number' }, category: { type: 'String' }, title: { type: 'String' },
- content: { type: 'String' }, contact: { type: 'String' }, payload: { type: 'Object' },
- status: { type: 'String' }, reviewedAt: { type: 'Date' }, handledBy: pointer('_User'),
- },
- CommerceProduct: {
- company: pointer('Company'), owner: pointer('_User'), legacyOwnerId: { type: 'Number' },
- productId: { type: 'Number' }, name: { type: 'String' }, nodeId: { type: 'Number' },
- retailPrice: { type: 'Number' }, marketPrice: { type: 'Number' }, unit: { type: 'String' },
- stock: { type: 'Number' }, thumbnail: { type: 'String' }, gallery: { type: 'Array' },
- summary: { type: 'String' }, content: { type: 'String' }, status: { type: 'String' },
- sales: { type: 'Number' }, hot: { type: 'Boolean' }, isDeleted: { type: 'Boolean' }, sourceKey: { type: 'String' },
- },
- ShoppingCartItem: {
- ...shared,
- product: pointer('CommerceProduct'), productId: { type: 'Number' }, quantity: { type: 'Number' }, selected: { type: 'Boolean' },
- },
- ProductStockLedger: {
- company: pointer('Company'), user: pointer('_User'), legacyUserId: { type: 'Number' },
- product: pointer('CommerceProduct'), productId: { type: 'Number' }, delta: { type: 'Number' },
- stockBefore: { type: 'Number' }, stockAfter: { type: 'Number' }, remark: { type: 'String' }, sourceKey: { type: 'String' },
- },
- ProductComment: {
- ...shared,
- product: pointer('CommerceProduct'), productId: { type: 'Number' }, orderId: { type: 'String' },
- title: { type: 'String' }, score: { type: 'Number' }, content: { type: 'String' }, images: { type: 'Array' },
- anonymous: { type: 'Boolean' }, status: { type: 'String' },
- },
- CommerceOrder: {
- company: pointer('Company'), user: pointer('_User'), buyerLegacyId: { type: 'Number' },
- orderId: { type: 'Number' }, orderNo: { type: 'String' }, status: { type: 'String' },
- subtotal: { type: 'Number' }, discount: { type: 'Number' }, shipping: { type: 'Number' }, total: { type: 'Number' },
- receiver: { type: 'Object' }, invoice: { type: 'Object' }, couponId: { type: 'String' }, paymentMode: { type: 'String' },
- paidAt: { type: 'Date' }, shippedAt: { type: 'Date' }, signedAt: { type: 'Date' },
- expressCompany: { type: 'String' }, expressNo: { type: 'String' }, expressRemark: { type: 'String' },
- isDeleted: { type: 'Boolean' }, sourceKey: { type: 'String' },
- },
- CommerceOrderItem: {
- company: pointer('Company'), order: pointer('CommerceOrder'), orderId: { type: 'Number' },
- product: pointer('CommerceProduct'), productId: { type: 'Number' }, seller: pointer('_User'), sellerLegacyId: { type: 'Number' },
- name: { type: 'String' }, unitPrice: { type: 'Number' }, quantity: { type: 'Number' }, amount: { type: 'Number' },
- thumbnail: { type: 'String' }, productSnapshot: { type: 'Object' }, sourceKey: { type: 'String' },
- },
- CommercePayment: {
- company: pointer('Company'), user: pointer('_User'), legacyUserId: { type: 'Number' },
- order: pointer('CommerceOrder'), orderId: { type: 'Number' }, amount: { type: 'Number' }, mode: { type: 'String' },
- status: { type: 'String' }, transactionNo: { type: 'String' }, paidAt: { type: 'Date' }, sourceKey: { type: 'String' },
- },
- CouponDefinition: {
- company: pointer('Company'), couponId: { type: 'Number' }, name: { type: 'String' }, amount: { type: 'Number' },
- minAmount: { type: 'Number' }, maxAmount: { type: 'Number' }, startsAt: { type: 'Date' }, expiresAt: { type: 'Date' },
- status: { type: 'String' }, totalQuantity: { type: 'Number' }, claimedQuantity: { type: 'Number' }, sourceKey: { type: 'String' },
- },
- UserCoupon: {
- ...shared,
- coupon: pointer('CouponDefinition'), couponId: { type: 'Number' }, status: { type: 'String' },
- usedAt: { type: 'Date' }, expiresAt: { type: 'Date' },
- },
- MembershipStockLedger: {
- company: pointer('Company'), operator: pointer('_User'), operatorLegacyId: { type: 'Number' },
- target: pointer('_User'), targetLegacyId: { type: 'Number' }, type: { type: 'String' },
- groupBefore: { type: 'Number' }, groupAfter: { type: 'Number' }, quantity: { type: 'Number' },
- operatorStockBefore: { type: 'Number' }, operatorStockAfter: { type: 'Number' },
- targetStockBefore: { type: 'Number' }, targetStockAfter: { type: 'Number' },
- remark: { type: 'String' }, sourceKey: { type: 'String' },
- },
- AppAuditLog: {
- company: pointer('Company'), user: pointer('_User'), legacyUserId: { type: 'Number' },
- action: { type: 'String' }, targetClass: { type: 'String' }, targetId: { type: 'String' },
- before: { type: 'Object' }, after: { type: 'Object' }, ip: { type: 'String' }, sourceKey: { type: 'String' },
- },
- LegacySyncCheckpoint: {
- company: pointer('Company'), sourceKey: { type: 'String' }, dataset: { type: 'String' },
- cursor: { type: 'String' }, state: { type: 'String' }, processedCount: { type: 'Number' },
- lastBatchCount: { type: 'Number' }, lastFailureCount: { type: 'Number' },
- lastAttemptAt: { type: 'Date' }, lastSuccessAt: { type: 'Date' },
- leaseToken: { type: 'String' }, leaseUntil: { type: 'Date' },
- lastServerTime: { type: 'Date' }, lastWatermark: { type: 'Number' },
- },
- LegacySyncConflict: {
- company: pointer('Company'), sourceKey: { type: 'String' }, dataset: { type: 'String' },
- legacyKey: { type: 'String' }, conflictType: { type: 'String' }, message: { type: 'String' },
- resolved: { type: 'Boolean' }, resolvedAt: { type: 'Date' }, resolution: { type: 'String' },
- },
- LegacySyncDeadLetter: {
- company: pointer('Company'), sourceKey: { type: 'String' }, dataset: { type: 'String' },
- legacyKey: { type: 'String' }, recordClasses: { type: 'Array' }, errorCode: { type: 'Number' },
- errorMessage: { type: 'String' }, attempts: { type: 'Number' }, resolved: { type: 'Boolean' },
- lastAttemptAt: { type: 'Date' }, resolvedAt: { type: 'Date' },
- },
- };
- async function request(path, init = {}) {
- const response = await fetch(`${PARSE_URL}${path}`, { ...init, headers: { ...headers, ...(init.headers || {}) } });
- const payload = await response.json().catch(() => ({}));
- if (!response.ok || payload.error) throw new Error(`${path}: ${payload.error || response.status}`);
- return payload;
- }
- for (const [className, fields] of Object.entries(schemas)) {
- const existingResponse = await fetch(`${PARSE_URL}/schemas/${className}`, { headers });
- const existing = await existingResponse.json().catch(() => ({}));
- if (existingResponse.status === 404 || (!existingResponse.ok && /does not exist/i.test(String(existing.error || '')))) {
- await request('/schemas/' + className, {
- method: 'POST',
- body: JSON.stringify({ className, fields, classLevelPermissions: lockedPermissions }),
- });
- console.log(`created ${className}`);
- continue;
- }
- if (!existingResponse.ok || existing.error) throw new Error(`${className}: ${existing.error || existingResponse.status}`);
- const additions = Object.fromEntries(Object.entries(fields).filter(([name]) => !existing.fields?.[name]));
- if (Object.keys(additions).length) {
- await request('/schemas/' + className, { method: 'PUT', body: JSON.stringify({ fields: additions }) });
- console.log(`updated ${className}: ${Object.keys(additions).join(', ')}`);
- } else console.log(`current ${className}`);
- }
|