payment-kit 1.27.1 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__blocklet__.js +37 -0
- package/api/ocap-1.30-subpath-shims.d.ts +35 -0
- package/api/src/crons/index.ts +10 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- package/api/src/integrations/arcblock/nft.ts +6 -2
- package/api/src/integrations/arcblock/stake.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +4 -4
- package/api/src/integrations/blocklet/notification.ts +1 -1
- package/api/src/integrations/ethereum/tx.ts +29 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
- package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
- package/api/src/integrations/stripe/resource.ts +8 -0
- package/api/src/libs/audit.ts +32 -16
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -0
- package/api/src/libs/error.ts +15 -0
- package/api/src/libs/event.ts +42 -1
- package/api/src/libs/invoice.ts +69 -34
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/pagination.ts +14 -9
- package/api/src/libs/payment.ts +25 -10
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +16 -15
- package/api/src/libs/wallet-migration.ts +72 -53
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/credit-consume.ts +94 -12
- package/api/src/queues/credit-grant.ts +4 -0
- package/api/src/queues/event.ts +14 -2
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +83 -15
- package/api/src/queues/refund.ts +84 -71
- package/api/src/queues/subscription.ts +1 -0
- package/api/src/routes/checkout-sessions.ts +82 -43
- package/api/src/routes/connect/change-payment.ts +2 -0
- package/api/src/routes/connect/change-plan.ts +2 -0
- package/api/src/routes/connect/pay.ts +12 -3
- package/api/src/routes/connect/setup.ts +3 -1
- package/api/src/routes/connect/shared.ts +52 -39
- package/api/src/routes/connect/subscribe.ts +4 -1
- package/api/src/routes/credit-grants.ts +25 -17
- package/api/src/routes/donations.ts +2 -2
- package/api/src/routes/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +1 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/models/customer.ts +23 -1
- package/api/src/store/models/payment-method.ts +4 -0
- package/api/src/store/models/price.ts +23 -14
- package/api/tests/libs/wallet-migration.spec.ts +4 -4
- package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
- package/api/tests/queues/credit-consume.spec.ts +8 -4
- package/api/tests/routes/credit-grants.spec.ts +1 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
- package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
- package/cloudflare/README.md +499 -0
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
- package/cloudflare/build.ts +151 -0
- package/cloudflare/did-connect-auth.ts +527 -0
- package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
- package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
- package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
- package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
- package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
- package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
- package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
- package/cloudflare/frontend-shims/js-sdk.ts +43 -0
- package/cloudflare/frontend-shims/mime-types.ts +46 -0
- package/cloudflare/frontend-shims/session.ts +24 -0
- package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
- package/cloudflare/index.html +40 -0
- package/cloudflare/migrate-to-d1.js +252 -0
- package/cloudflare/migrations/0001_initial_schema.sql +82 -0
- package/cloudflare/migrations/0002_indexes.sql +75 -0
- package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
- package/cloudflare/run-build.js +390 -0
- package/cloudflare/scripts/test-decrypt.js +102 -0
- package/cloudflare/shims/arcblock-ws.ts +20 -0
- package/cloudflare/shims/axios-http-adapter.ts +4 -0
- package/cloudflare/shims/axios-lite.ts +117 -0
- package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
- package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
- package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
- package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
- package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
- package/cloudflare/shims/cookie-parser.ts +3 -0
- package/cloudflare/shims/cors.ts +21 -0
- package/cloudflare/shims/cron.ts +189 -0
- package/cloudflare/shims/crypto-js-warn.ts +7 -0
- package/cloudflare/shims/did-space-js.ts +17 -0
- package/cloudflare/shims/did-space.ts +11 -0
- package/cloudflare/shims/error.ts +18 -0
- package/cloudflare/shims/express-compat/index.ts +80 -0
- package/cloudflare/shims/express-compat/types.ts +41 -0
- package/cloudflare/shims/fastq.ts +105 -0
- package/cloudflare/shims/lock.ts +115 -0
- package/cloudflare/shims/mime-types.ts +56 -0
- package/cloudflare/shims/nedb-storage.ts +9 -0
- package/cloudflare/shims/node-child-process.ts +9 -0
- package/cloudflare/shims/node-fs.ts +20 -0
- package/cloudflare/shims/node-http.ts +13 -0
- package/cloudflare/shims/node-https.ts +4 -0
- package/cloudflare/shims/node-misc.ts +15 -0
- package/cloudflare/shims/node-net.ts +8 -0
- package/cloudflare/shims/node-os.ts +14 -0
- package/cloudflare/shims/node-tty.ts +8 -0
- package/cloudflare/shims/node-zlib.ts +17 -0
- package/cloudflare/shims/noop.ts +26 -0
- package/cloudflare/shims/payment-vendor.ts +14 -0
- package/cloudflare/shims/querystring.ts +12 -0
- package/cloudflare/shims/queue.ts +585 -0
- package/cloudflare/shims/rolldown-runtime.ts +43 -0
- package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
- package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
- package/cloudflare/shims/sequelize-d1/index.ts +34 -0
- package/cloudflare/shims/sequelize-d1/model.ts +1157 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +293 -0
- package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
- package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
- package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
- package/cloudflare/shims/sequelize-d1/types.ts +35 -0
- package/cloudflare/shims/stripe-cf.ts +29 -0
- package/cloudflare/shims/ws-lite.ts +103 -0
- package/cloudflare/shims/xss.ts +3 -0
- package/cloudflare/tests/shims/cron.spec.ts +210 -0
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1553 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +69 -0
- package/cloudflare/wrangler.staging.json +66 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +26 -22
- package/src/app.tsx +62 -4
- package/src/components/customer/link.tsx +9 -13
- package/src/components/customer/notification-preference.tsx +3 -2
- package/src/components/filter-toolbar.tsx +4 -0
- package/src/components/invoice/list.tsx +9 -1
- package/src/components/invoice-pdf/utils.ts +2 -1
- package/src/components/layout/admin.tsx +39 -5
- package/src/components/layout/user-cf.tsx +77 -0
- package/src/components/payment-intent/actions.tsx +23 -3
- package/src/components/safe-did-address.tsx +75 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/customers/customers/detail.tsx +2 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/tsconfig.api.json +1 -6
- package/tsconfig.json +3 -4
- package/tsconfig.types.json +2 -1
- package/vite.config.ts +6 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Sequelize Op → SQL WHERE clause translation
|
|
2
|
+
|
|
3
|
+
export const Op = {
|
|
4
|
+
eq: Symbol('eq'),
|
|
5
|
+
ne: Symbol('ne'),
|
|
6
|
+
gt: Symbol('gt'),
|
|
7
|
+
gte: Symbol('gte'),
|
|
8
|
+
lt: Symbol('lt'),
|
|
9
|
+
lte: Symbol('lte'),
|
|
10
|
+
in: Symbol('in'),
|
|
11
|
+
notIn: Symbol('notIn'),
|
|
12
|
+
like: Symbol('like'),
|
|
13
|
+
notLike: Symbol('notLike'),
|
|
14
|
+
or: Symbol('or'),
|
|
15
|
+
and: Symbol('and'),
|
|
16
|
+
not: Symbol('not'),
|
|
17
|
+
between: Symbol('between'),
|
|
18
|
+
notBetween: Symbol('notBetween'),
|
|
19
|
+
is: Symbol('is'),
|
|
20
|
+
overlap: Symbol('overlap'),
|
|
21
|
+
contains: Symbol('contains'),
|
|
22
|
+
startsWith: Symbol('startsWith'),
|
|
23
|
+
endsWith: Symbol('endsWith'),
|
|
24
|
+
substring: Symbol('substring'),
|
|
25
|
+
col: Symbol('col'),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const OP_MAP = new Map<symbol, string>([
|
|
29
|
+
[Op.eq, '='],
|
|
30
|
+
[Op.ne, '!='],
|
|
31
|
+
[Op.gt, '>'],
|
|
32
|
+
[Op.gte, '>='],
|
|
33
|
+
[Op.lt, '<'],
|
|
34
|
+
[Op.lte, '<='],
|
|
35
|
+
[Op.like, 'LIKE'],
|
|
36
|
+
[Op.notLike, 'NOT LIKE'],
|
|
37
|
+
[Op.is, 'IS'],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const SYM_SET = new Set<symbol>(Object.values(Op) as symbol[]);
|
|
41
|
+
|
|
42
|
+
function isOpSymbol(s: symbol): boolean {
|
|
43
|
+
return SYM_SET.has(s);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Inline helper to convert literal/fn/col to SQL — avoids circular import with helpers.ts
|
|
47
|
+
function helperToSQLInline(value: any): string {
|
|
48
|
+
if (!value || typeof value !== 'object') return String(value);
|
|
49
|
+
if (value.__literal) return value.sql;
|
|
50
|
+
if (value.__col) {
|
|
51
|
+
if (value.name.includes('.')) return `"${value.name.replace('.', '"."')}"`;
|
|
52
|
+
return `"${value.name}"`;
|
|
53
|
+
}
|
|
54
|
+
if (value.__fn) {
|
|
55
|
+
const args = value.args.map((a: any) => {
|
|
56
|
+
if (a === null || a === undefined) return 'NULL';
|
|
57
|
+
if (typeof a === 'string') return `"${a}"`;
|
|
58
|
+
if (typeof a === 'number') return String(a);
|
|
59
|
+
return helperToSQLInline(a);
|
|
60
|
+
});
|
|
61
|
+
return `${value.name}(${args.join(', ')})`;
|
|
62
|
+
}
|
|
63
|
+
return String(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Convert a dotted field path like 'metadata.stripe_id' to SQLite json_extract expression
|
|
67
|
+
function fieldToSQL(field: string): string {
|
|
68
|
+
if (field.includes('.')) {
|
|
69
|
+
const parts = field.split('.');
|
|
70
|
+
const column = parts[0];
|
|
71
|
+
const jsonPath = '$.' + parts.slice(1).join('.');
|
|
72
|
+
return `json_extract("${column}", '${jsonPath}')`;
|
|
73
|
+
}
|
|
74
|
+
return `"${field}"`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle a single operator condition for a field
|
|
78
|
+
function buildOpCondition(
|
|
79
|
+
fieldSQL: string,
|
|
80
|
+
sym: symbol,
|
|
81
|
+
opValue: any,
|
|
82
|
+
conditions: string[],
|
|
83
|
+
values: any[],
|
|
84
|
+
): void {
|
|
85
|
+
if (sym === Op.in) {
|
|
86
|
+
const arr = opValue as any[];
|
|
87
|
+
if (arr.length === 0) {
|
|
88
|
+
conditions.push('1=0');
|
|
89
|
+
} else {
|
|
90
|
+
const placeholders = arr.map(() => '?').join(', ');
|
|
91
|
+
conditions.push(`${fieldSQL} IN (${placeholders})`);
|
|
92
|
+
values.push(...arr);
|
|
93
|
+
}
|
|
94
|
+
} else if (sym === Op.notIn) {
|
|
95
|
+
const arr = opValue as any[];
|
|
96
|
+
if (arr.length === 0) {
|
|
97
|
+
conditions.push('1=1');
|
|
98
|
+
} else {
|
|
99
|
+
const placeholders = arr.map(() => '?').join(', ');
|
|
100
|
+
conditions.push(`${fieldSQL} NOT IN (${placeholders})`);
|
|
101
|
+
values.push(...arr);
|
|
102
|
+
}
|
|
103
|
+
} else if (sym === Op.between) {
|
|
104
|
+
conditions.push(`${fieldSQL} BETWEEN ? AND ?`);
|
|
105
|
+
values.push(opValue[0], opValue[1]);
|
|
106
|
+
} else if (sym === Op.notBetween) {
|
|
107
|
+
conditions.push(`${fieldSQL} NOT BETWEEN ? AND ?`);
|
|
108
|
+
values.push(opValue[0], opValue[1]);
|
|
109
|
+
} else if (sym === Op.not) {
|
|
110
|
+
if (opValue === null) {
|
|
111
|
+
conditions.push(`${fieldSQL} IS NOT NULL`);
|
|
112
|
+
} else {
|
|
113
|
+
conditions.push(`${fieldSQL} != ?`);
|
|
114
|
+
values.push(opValue);
|
|
115
|
+
}
|
|
116
|
+
} else if (sym === Op.is) {
|
|
117
|
+
if (opValue === null) {
|
|
118
|
+
conditions.push(`${fieldSQL} IS NULL`);
|
|
119
|
+
} else {
|
|
120
|
+
conditions.push(`${fieldSQL} IS ?`);
|
|
121
|
+
values.push(opValue);
|
|
122
|
+
}
|
|
123
|
+
} else if (sym === Op.or) {
|
|
124
|
+
const orParts = (opValue as any[]).map(() => `${fieldSQL} = ?`);
|
|
125
|
+
conditions.push(`(${orParts.join(' OR ')})`);
|
|
126
|
+
values.push(...(opValue as any[]));
|
|
127
|
+
} else if (sym === Op.contains) {
|
|
128
|
+
// JSON array contains check using SQLite json_each
|
|
129
|
+
const searchVal = Array.isArray(opValue) ? opValue[0] : opValue;
|
|
130
|
+
conditions.push(`EXISTS (SELECT 1 FROM json_each(${fieldSQL}) WHERE value = ?)`);
|
|
131
|
+
values.push(searchVal);
|
|
132
|
+
} else {
|
|
133
|
+
const sqlOp = OP_MAP.get(sym);
|
|
134
|
+
if (sqlOp) {
|
|
135
|
+
conditions.push(`${fieldSQL} ${sqlOp} ?`);
|
|
136
|
+
values.push(opValue);
|
|
137
|
+
} else {
|
|
138
|
+
conditions.push(`${fieldSQL} = ?`);
|
|
139
|
+
values.push(opValue);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// sequelizeWhere: wraps Sequelize.where(lhs, rhs) calls
|
|
145
|
+
export function sequelizeWhere(lhs: any, rhs: any): any {
|
|
146
|
+
return { __sequelizeWhere: true, lhs, rhs };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildWhereClause(where: any): { sql: string; values: any[] } {
|
|
150
|
+
if (!where) return { sql: '', values: [] };
|
|
151
|
+
|
|
152
|
+
const conditions: string[] = [];
|
|
153
|
+
const values: any[] = [];
|
|
154
|
+
|
|
155
|
+
// Special case: Sequelize.where(literal(...), value)
|
|
156
|
+
if (where.__sequelizeWhere) {
|
|
157
|
+
const { lhs, rhs } = where;
|
|
158
|
+
const lhsSQL = typeof lhs === 'object' ? helperToSQLInline(lhs) : `"${lhs}"`;
|
|
159
|
+
conditions.push(`${lhsSQL} = ?`);
|
|
160
|
+
values.push(rhs);
|
|
161
|
+
return { sql: conditions.join(' AND '), values };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Array case
|
|
165
|
+
if (Array.isArray(where)) {
|
|
166
|
+
const parts = where.map((item) => {
|
|
167
|
+
const sub = buildWhereClause(item);
|
|
168
|
+
values.push(...sub.values);
|
|
169
|
+
return sub.sql ? `(${sub.sql})` : '1=1';
|
|
170
|
+
});
|
|
171
|
+
return { sql: parts.join(' AND '), values };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Collect string keys
|
|
175
|
+
const stringEntries: [string, any][] = Object.entries(where);
|
|
176
|
+
// Collect Symbol keys — critical for { [Op.or]: [...] } top-level usage
|
|
177
|
+
const symbolKeys: symbol[] = Object.getOwnPropertySymbols(where);
|
|
178
|
+
|
|
179
|
+
for (const [key, value] of stringEntries) {
|
|
180
|
+
processWhereEntry(key, value, conditions, values);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const sym of symbolKeys) {
|
|
184
|
+
if (!isOpSymbol(sym)) continue;
|
|
185
|
+
const value = (where as any)[sym];
|
|
186
|
+
|
|
187
|
+
if (sym === Op.or) {
|
|
188
|
+
const items = Array.isArray(value) ? value : [value];
|
|
189
|
+
const orParts = items.map((v) => {
|
|
190
|
+
const sub = buildWhereClause(v);
|
|
191
|
+
values.push(...sub.values);
|
|
192
|
+
return sub.sql ? `(${sub.sql})` : '1=1';
|
|
193
|
+
});
|
|
194
|
+
if (orParts.length > 0) conditions.push(`(${orParts.join(' OR ')})`);
|
|
195
|
+
} else if (sym === Op.and) {
|
|
196
|
+
const items = Array.isArray(value) ? value : [value];
|
|
197
|
+
const andParts = items.map((v) => {
|
|
198
|
+
if (v && v.__sequelizeWhere) {
|
|
199
|
+
const sub = buildWhereClause(v);
|
|
200
|
+
values.push(...sub.values);
|
|
201
|
+
return sub.sql ? `(${sub.sql})` : '1=1';
|
|
202
|
+
}
|
|
203
|
+
if (v && v.__literal) {
|
|
204
|
+
return `(${v.sql})`;
|
|
205
|
+
}
|
|
206
|
+
const sub = buildWhereClause(v);
|
|
207
|
+
values.push(...sub.values);
|
|
208
|
+
return sub.sql ? `(${sub.sql})` : '1=1';
|
|
209
|
+
});
|
|
210
|
+
if (andParts.length > 0) conditions.push(`(${andParts.join(' AND ')})`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
sql: conditions.length > 0 ? conditions.join(' AND ') : '',
|
|
216
|
+
values,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function processWhereEntry(key: string, value: any, conditions: string[], values: any[]): void {
|
|
221
|
+
const fieldSQL = fieldToSQL(key);
|
|
222
|
+
|
|
223
|
+
if (key === String(Op.or)) {
|
|
224
|
+
const items = Array.isArray(value) ? value : [value];
|
|
225
|
+
const orParts = items.map((v) => {
|
|
226
|
+
const sub = buildWhereClause(v);
|
|
227
|
+
values.push(...sub.values);
|
|
228
|
+
return sub.sql ? `(${sub.sql})` : '1=1';
|
|
229
|
+
});
|
|
230
|
+
if (orParts.length > 0) conditions.push(`(${orParts.join(' OR ')})`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (key === String(Op.and)) {
|
|
235
|
+
const items = Array.isArray(value) ? value : [value];
|
|
236
|
+
const andParts = items.map((v) => {
|
|
237
|
+
const sub = buildWhereClause(v);
|
|
238
|
+
values.push(...sub.values);
|
|
239
|
+
return sub.sql ? `(${sub.sql})` : '1=1';
|
|
240
|
+
});
|
|
241
|
+
if (andParts.length > 0) conditions.push(`(${andParts.join(' AND ')})`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (value === null || value === undefined) {
|
|
246
|
+
conditions.push(`${fieldSQL} IS NULL`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (typeof value !== 'object' || value instanceof Date) {
|
|
251
|
+
conditions.push(`${fieldSQL} = ?`);
|
|
252
|
+
values.push(value instanceof Date ? value.toISOString() : value);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (value.__sequelizeWhere) {
|
|
257
|
+
const sub = buildWhereClause(value);
|
|
258
|
+
if (sub.sql) {
|
|
259
|
+
conditions.push(sub.sql);
|
|
260
|
+
values.push(...sub.values);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (value.__literal) {
|
|
266
|
+
conditions.push(`${fieldSQL} = (${value.sql})`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const symKeys = Object.getOwnPropertySymbols(value);
|
|
271
|
+
if (symKeys.length > 0) {
|
|
272
|
+
for (const sym of symKeys) {
|
|
273
|
+
if (!isOpSymbol(sym)) continue;
|
|
274
|
+
buildOpCondition(fieldSQL, sym, (value as any)[sym], conditions, values);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
if (value.length === 0) {
|
|
281
|
+
conditions.push('1=0');
|
|
282
|
+
} else {
|
|
283
|
+
const placeholders = value.map(() => '?').join(', ');
|
|
284
|
+
conditions.push(`${fieldSQL} IN (${placeholders})`);
|
|
285
|
+
values.push(...value);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Nested plain object — JSON equality
|
|
291
|
+
conditions.push(`${fieldSQL} = ?`);
|
|
292
|
+
values.push(JSON.stringify(value));
|
|
293
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1 auto-retry wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare D1 occasionally returns transient errors like:
|
|
5
|
+
* - "Network connection lost"
|
|
6
|
+
* - "503 Service Unavailable"
|
|
7
|
+
* - connection timeouts
|
|
8
|
+
*
|
|
9
|
+
* These are usually recoverable within 50-100ms. This Proxy wraps the D1 binding
|
|
10
|
+
* and automatically retries such errors once with a small backoff.
|
|
11
|
+
*
|
|
12
|
+
* Non-transient errors (SQL syntax, constraint violations, etc.) are re-thrown
|
|
13
|
+
* immediately to avoid masking real bugs.
|
|
14
|
+
*
|
|
15
|
+
* Usage in worker.ts:
|
|
16
|
+
* setDB(withD1Retry(c.env.DB.withSession('first-primary')));
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const TRANSIENT_ERROR_PATTERNS = [
|
|
20
|
+
/network connection lost/i,
|
|
21
|
+
/503/,
|
|
22
|
+
/timeout/i,
|
|
23
|
+
/ECONN/,
|
|
24
|
+
/socket hang up/i,
|
|
25
|
+
/internal error/i,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function isTransientError(err: any): boolean {
|
|
29
|
+
const msg = String(err?.message || err || '');
|
|
30
|
+
return TRANSIENT_ERROR_PATTERNS.some((p) => p.test(msg));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function retryAsync<T>(fn: () => Promise<T>, maxRetries = 1): Promise<T> {
|
|
34
|
+
let lastErr: any;
|
|
35
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
36
|
+
try {
|
|
37
|
+
return await fn();
|
|
38
|
+
} catch (err: any) {
|
|
39
|
+
lastErr = err;
|
|
40
|
+
if (!isTransientError(err)) throw err;
|
|
41
|
+
if (i < maxRetries) {
|
|
42
|
+
console.warn(`[D1 retry] transient error, retrying (${i + 1}/${maxRetries}):`, err?.message || err);
|
|
43
|
+
// Small jittered backoff: 50-100ms on first retry
|
|
44
|
+
const delay = 50 + Math.random() * 50;
|
|
45
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw lastErr;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function wrapStatement(stmt: any): any {
|
|
53
|
+
return new Proxy(stmt, {
|
|
54
|
+
get(target, prop, receiver) {
|
|
55
|
+
if (prop === 'bind') {
|
|
56
|
+
return (...args: any[]) => wrapStatement(target.bind(...args));
|
|
57
|
+
}
|
|
58
|
+
if (prop === 'all' || prop === 'run' || prop === 'first' || prop === 'raw') {
|
|
59
|
+
return (...args: any[]) => retryAsync(() => (target as any)[prop](...args));
|
|
60
|
+
}
|
|
61
|
+
return Reflect.get(target, prop, receiver);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Wrap a D1Database binding so all queries auto-retry on transient errors. */
|
|
67
|
+
export function withD1Retry(db: any): any {
|
|
68
|
+
return new Proxy(db, {
|
|
69
|
+
get(target, prop, receiver) {
|
|
70
|
+
if (prop === 'prepare') {
|
|
71
|
+
return (sql: string) => wrapStatement(target.prepare(sql));
|
|
72
|
+
}
|
|
73
|
+
if (prop === 'batch') {
|
|
74
|
+
return (stmts: any[]) => retryAsync(() => target.batch(stmts));
|
|
75
|
+
}
|
|
76
|
+
if (prop === 'exec') {
|
|
77
|
+
return (sql: string) => retryAsync(() => target.exec(sql));
|
|
78
|
+
}
|
|
79
|
+
if (prop === 'withSession') {
|
|
80
|
+
return (...args: any[]) => withD1Retry(target.withSession(...args));
|
|
81
|
+
}
|
|
82
|
+
return Reflect.get(target, prop, receiver);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Sequelize class shim — replaces `new Sequelize({...})` and `sequelize.transaction()`
|
|
2
|
+
import { getDB, modelRegistry, sharedModels } from './model';
|
|
3
|
+
import { helperToSQL } from './helpers';
|
|
4
|
+
import { sequelizeWhere } from './operators';
|
|
5
|
+
|
|
6
|
+
export class Sequelize {
|
|
7
|
+
// models registry — shared across all instances via the sharedModels singleton from model.ts
|
|
8
|
+
models: Record<string, any> = sharedModels;
|
|
9
|
+
|
|
10
|
+
constructor(_options?: any) {
|
|
11
|
+
// No-op in CF — D1 is injected via env binding
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static useCLS(_namespace: any) {
|
|
15
|
+
// No-op — CLS not available/needed in CF Workers
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async query(sql: string, options?: any): Promise<any> {
|
|
19
|
+
const db = getDB();
|
|
20
|
+
let finalSql = sql;
|
|
21
|
+
const bindValues: any[] = [];
|
|
22
|
+
|
|
23
|
+
// Handle named replacements: { replacements: { key: value } }
|
|
24
|
+
if (options?.replacements && typeof options.replacements === 'object' && !Array.isArray(options.replacements)) {
|
|
25
|
+
const replacements = options.replacements as Record<string, any>;
|
|
26
|
+
finalSql = sql.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
27
|
+
const val = replacements[name];
|
|
28
|
+
if (val === null || val === undefined) return 'NULL';
|
|
29
|
+
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
|
|
30
|
+
return String(val);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// Handle positional replacements: { replacements: [val1, val2] }
|
|
34
|
+
else if (Array.isArray(options?.replacements)) {
|
|
35
|
+
const positional = options.replacements as any[];
|
|
36
|
+
let idx = 0;
|
|
37
|
+
finalSql = sql.replace(/\?/g, () => {
|
|
38
|
+
const val = positional[idx++];
|
|
39
|
+
if (val === null || val === undefined) return 'NULL';
|
|
40
|
+
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
|
|
41
|
+
return String(val);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Handle bind params: { bind: [...] } — use D1's prepared statement bind
|
|
45
|
+
else if (options?.bind) {
|
|
46
|
+
const bind = Array.isArray(options.bind) ? options.bind : Object.values(options.bind);
|
|
47
|
+
// Replace $1, $2... or named $name with ?
|
|
48
|
+
finalSql = sql.replace(/\$[0-9]+|\$[a-zA-Z_][a-zA-Z0-9_]*/g, '?');
|
|
49
|
+
bindValues.push(...bind);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stmt = bindValues.length > 0
|
|
53
|
+
? db.prepare(finalSql).bind(...bindValues)
|
|
54
|
+
: db.prepare(finalSql);
|
|
55
|
+
|
|
56
|
+
const result = await stmt.all();
|
|
57
|
+
|
|
58
|
+
if (options?.type === 'SELECT') {
|
|
59
|
+
return result.results || [];
|
|
60
|
+
}
|
|
61
|
+
return [result.results || [], result.meta];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async transaction(fn?: any): Promise<any> {
|
|
65
|
+
// D1 doesn't support real transactions in the same way
|
|
66
|
+
// Execute callback directly with a fake transaction object
|
|
67
|
+
const fakeTx = {
|
|
68
|
+
commit: async () => {},
|
|
69
|
+
rollback: async () => {},
|
|
70
|
+
LOCK: { UPDATE: 'UPDATE', SHARE: 'SHARE' },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (typeof fn === 'function') {
|
|
74
|
+
try {
|
|
75
|
+
return await fn(fakeTx);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Called without fn — return transaction object (caller manages commit/rollback)
|
|
82
|
+
return fakeTx;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Instance versions of static helpers (used as `sequelize.fn()`, `sequelize.literal()`, etc.)
|
|
86
|
+
fn(name: string, ...args: any[]) {
|
|
87
|
+
return { __fn: true, name, args };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
literal(sql: string) {
|
|
91
|
+
return { __literal: true, sql };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
col(name: string) {
|
|
95
|
+
return { __col: true, name };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// sequelize.where(fn/literal, value) — used in WHERE clauses
|
|
99
|
+
where(lhs: any, rhs: any): any {
|
|
100
|
+
return sequelizeWhere(lhs, rhs);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Static versions — used as Sequelize.fn(), Sequelize.literal(), etc.
|
|
104
|
+
static fn(name: string, ...args: any[]) {
|
|
105
|
+
return { __fn: true, name, args };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static literal(sql: string) {
|
|
109
|
+
return { __literal: true, sql };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static col(name: string) {
|
|
113
|
+
return { __col: true, name };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static where(lhs: any, rhs: any): any {
|
|
117
|
+
return sequelizeWhere(lhs, rhs);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Per-request D1 timing accumulator + named phase timings.
|
|
2
|
+
// CF Workers executes one request at a time per isolate, so globalThis is safe.
|
|
3
|
+
// Reset by middleware before route handler runs, read after for Server-Timing header.
|
|
4
|
+
|
|
5
|
+
export interface D1TimingContext {
|
|
6
|
+
queries: number;
|
|
7
|
+
sqlMs: number; // cumulative meta.duration (D1 SQL execution time)
|
|
8
|
+
wallMs: number; // cumulative performance.now() delta (includes D1 RTT)
|
|
9
|
+
rowsRead: number;
|
|
10
|
+
rowsWritten: number;
|
|
11
|
+
/** Named phase timings — for fine-grained Server-Timing breakdown */
|
|
12
|
+
phases: Record<string, number>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const EMPTY: D1TimingContext = {
|
|
16
|
+
queries: 0,
|
|
17
|
+
sqlMs: 0,
|
|
18
|
+
wallMs: 0,
|
|
19
|
+
rowsRead: 0,
|
|
20
|
+
rowsWritten: 0,
|
|
21
|
+
phases: {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function resetD1Timing(): void {
|
|
25
|
+
(globalThis as any).__d1Timing__ = { ...EMPTY, phases: {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getD1Timing(): D1TimingContext {
|
|
29
|
+
return (globalThis as any).__d1Timing__ || { ...EMPTY, phases: {} };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Measure a named phase and accumulate into the timing context.
|
|
34
|
+
* Usage:
|
|
35
|
+
* const result = await measurePhase('chain', () => checkTokenBalance(...));
|
|
36
|
+
*
|
|
37
|
+
* Multiple calls to the same phase name accumulate (additive).
|
|
38
|
+
*/
|
|
39
|
+
export async function measurePhase<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
|
40
|
+
const t0 = performance.now();
|
|
41
|
+
try {
|
|
42
|
+
return await fn();
|
|
43
|
+
} finally {
|
|
44
|
+
const t: D1TimingContext = (globalThis as any).__d1Timing__;
|
|
45
|
+
if (t) {
|
|
46
|
+
const dur = performance.now() - t0;
|
|
47
|
+
t.phases[name] = (t.phases[name] || 0) + dur;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Record a phase duration directly (for non-Promise code paths). */
|
|
53
|
+
export function recordPhase(name: string, durationMs: number): void {
|
|
54
|
+
const t: D1TimingContext = (globalThis as any).__d1Timing__;
|
|
55
|
+
if (!t) return;
|
|
56
|
+
t.phases[name] = (t.phases[name] || 0) + durationMs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Record a single D1 call (.all / .run / .first) */
|
|
60
|
+
export function recordQuery(meta: any, wallMs: number): void {
|
|
61
|
+
const t: D1TimingContext = (globalThis as any).__d1Timing__;
|
|
62
|
+
if (!t) return;
|
|
63
|
+
t.queries++;
|
|
64
|
+
t.sqlMs += meta?.duration ?? 0;
|
|
65
|
+
t.wallMs += wallMs;
|
|
66
|
+
t.rowsRead += meta?.rows_read ?? 0;
|
|
67
|
+
t.rowsWritten += meta?.rows_written ?? 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Record a db.batch() call — each result in the array has its own meta */
|
|
71
|
+
export function recordBatch(results: any[], wallMs: number): void {
|
|
72
|
+
const t: D1TimingContext = (globalThis as any).__d1Timing__;
|
|
73
|
+
if (!t) return;
|
|
74
|
+
for (const r of results) {
|
|
75
|
+
t.queries++;
|
|
76
|
+
t.sqlMs += r?.meta?.duration ?? 0;
|
|
77
|
+
t.rowsRead += r?.meta?.rows_read ?? 0;
|
|
78
|
+
t.rowsWritten += r?.meta?.rows_written ?? 0;
|
|
79
|
+
}
|
|
80
|
+
t.wallMs += wallMs;
|
|
81
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Minimal type stubs to satisfy Sequelize type imports
|
|
2
|
+
|
|
3
|
+
export type WhereOptions = Record<string | symbol, any>;
|
|
4
|
+
|
|
5
|
+
export type FindOptions = {
|
|
6
|
+
where?: WhereOptions;
|
|
7
|
+
include?: any[];
|
|
8
|
+
order?: any[];
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
attributes?: string[] | { include?: any[]; exclude?: string[] };
|
|
12
|
+
group?: string | string[];
|
|
13
|
+
having?: any;
|
|
14
|
+
transaction?: any;
|
|
15
|
+
lock?: any;
|
|
16
|
+
raw?: boolean;
|
|
17
|
+
paranoid?: boolean;
|
|
18
|
+
distinct?: boolean;
|
|
19
|
+
subQuery?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CreateOptions = { transaction?: any; returning?: boolean };
|
|
23
|
+
export type UpdateOptions = { where?: WhereOptions; transaction?: any; returning?: boolean };
|
|
24
|
+
export type DestroyOptions = { where?: WhereOptions; transaction?: any; truncate?: boolean };
|
|
25
|
+
|
|
26
|
+
// These are used as type-only in model declarations
|
|
27
|
+
export type InferAttributes<T, _Options extends object = never> = T;
|
|
28
|
+
export type InferCreationAttributes<T, _Options extends object = never> = Partial<T>;
|
|
29
|
+
export type CreationOptional<T> = T | undefined;
|
|
30
|
+
|
|
31
|
+
// Sequelize OrderItem type
|
|
32
|
+
export type OrderItem = [any, string] | string | any;
|
|
33
|
+
|
|
34
|
+
// ModelStatic type — used in some places
|
|
35
|
+
export type ModelStatic<M> = { new (): M; [key: string]: any };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Stripe shim for CF Workers
|
|
2
|
+
// Wraps the real Stripe constructor to auto-inject fetch HTTP client
|
|
3
|
+
// The real Stripe package is loaded directly (not through alias) via the
|
|
4
|
+
// esbuild plugin that excludes this file from the stripe alias.
|
|
5
|
+
|
|
6
|
+
// @ts-ignore — this import is resolved by esbuild to the real stripe package
|
|
7
|
+
import OriginalStripe from '__real_stripe__';
|
|
8
|
+
|
|
9
|
+
// Wrap constructor to inject fetch HTTP client
|
|
10
|
+
function StripeWrapper(this: any, apiKey: string, config: any = {}) {
|
|
11
|
+
const cfConfig = {
|
|
12
|
+
...config,
|
|
13
|
+
httpClient: (OriginalStripe as any).createFetchHttpClient(),
|
|
14
|
+
};
|
|
15
|
+
return new (OriginalStripe as any)(apiKey, cfConfig);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Copy all static properties
|
|
19
|
+
Object.keys(OriginalStripe).forEach((key) => {
|
|
20
|
+
(StripeWrapper as any)[key] = (OriginalStripe as any)[key];
|
|
21
|
+
});
|
|
22
|
+
Object.setPrototypeOf(StripeWrapper, OriginalStripe);
|
|
23
|
+
Object.setPrototypeOf(StripeWrapper.prototype, (OriginalStripe as any).prototype);
|
|
24
|
+
|
|
25
|
+
// Also copy createFetchHttpClient for direct usage
|
|
26
|
+
(StripeWrapper as any).createFetchHttpClient = (OriginalStripe as any).createFetchHttpClient;
|
|
27
|
+
|
|
28
|
+
export default StripeWrapper;
|
|
29
|
+
export { StripeWrapper as Stripe };
|