payment-kit 1.27.2 → 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,585 @@
|
|
|
1
|
+
// CF Workers queue shim — replaces libs/queue/index.ts
|
|
2
|
+
//
|
|
3
|
+
// Architecture (mirrors Blocklet Server's original design):
|
|
4
|
+
//
|
|
5
|
+
// D1 jobs table = scheduler (source of truth for all jobs)
|
|
6
|
+
// CF Queue = executor (replaces fastq — concurrent execution engine)
|
|
7
|
+
// Cron = dispatcher (replaces loop() — polls D1 for due jobs)
|
|
8
|
+
//
|
|
9
|
+
// Flow:
|
|
10
|
+
// push(job, immediate) → D1 addJob + CF Queue send → consumer executes → D1 deleteJob
|
|
11
|
+
// push(job, delayed) → D1 addJob only → cron dispatches when due → CF Queue → execute
|
|
12
|
+
// pushAndWait(job) → D1 addJob + inline execute (caller awaits result)
|
|
13
|
+
// cancel/delete → D1 deleteJob (delayed jobs never reach Queue until due)
|
|
14
|
+
// CF Queue 429 → job stays in D1, cron retries next cycle
|
|
15
|
+
//
|
|
16
|
+
// This preserves the original Blocklet Server semantics:
|
|
17
|
+
// fastq.push(job) → CF Queue send (immediate execution with concurrency control)
|
|
18
|
+
// store.addJob() → D1 jobs table (persistence + scheduling)
|
|
19
|
+
// loop() polling → cron every minute (dispatches due delayed jobs)
|
|
20
|
+
|
|
21
|
+
import EventEmitter from 'events';
|
|
22
|
+
import { nanoid } from 'nanoid';
|
|
23
|
+
import { Op } from 'sequelize';
|
|
24
|
+
|
|
25
|
+
import { Job, TJob } from '../../api/src/store/models/job';
|
|
26
|
+
import createQueueStore from '../../api/src/libs/queue/store';
|
|
27
|
+
|
|
28
|
+
// --- CF Queue binding ---
|
|
29
|
+
let _cfQueue: any | null = null;
|
|
30
|
+
|
|
31
|
+
export function setCFQueue(queue: any) {
|
|
32
|
+
_cfQueue = queue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Handler registry ---
|
|
36
|
+
type RegisteredHandler = {
|
|
37
|
+
onJob: (job: any) => Promise<any>;
|
|
38
|
+
executeJob: (jobId: string, job: any, persist: boolean) => Promise<any>;
|
|
39
|
+
store: ReturnType<typeof createQueueStore>;
|
|
40
|
+
cancel: (id: string) => Promise<any>;
|
|
41
|
+
};
|
|
42
|
+
const _handlers = new Map<string, RegisteredHandler>();
|
|
43
|
+
|
|
44
|
+
export function getHandler(queueName: string): RegisteredHandler | undefined {
|
|
45
|
+
return _handlers.get(queueName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getAllHandlerNames(): string[] {
|
|
49
|
+
return Array.from(_handlers.keys());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- waitUntil for keeping async work alive ---
|
|
53
|
+
let _waitUntil: ((promise: Promise<any>) => void) | null = null;
|
|
54
|
+
|
|
55
|
+
export function setWaitUntil(fn: (promise: Promise<any>) => void) {
|
|
56
|
+
_waitUntil = fn;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Pending promises — await before returning response
|
|
60
|
+
const _pendingPushJobs: Promise<any>[] = [];
|
|
61
|
+
(globalThis as any).__cfPendingJobs__ = _pendingPushJobs;
|
|
62
|
+
|
|
63
|
+
// --- Request-time timer tracking ---
|
|
64
|
+
// Business code uses setTimeout for batching (e.g. addToBatch's 3-second window).
|
|
65
|
+
// In CF Workers, these timers fire after the response is sent — too late.
|
|
66
|
+
// We intercept setTimeout during request handling, and flush pending timer
|
|
67
|
+
// callbacks in flushPendingJobs before the response returns.
|
|
68
|
+
|
|
69
|
+
type TrackedTimer = { fn: Function; args: any[]; cleared: boolean; realId: any };
|
|
70
|
+
const _trackedTimers: TrackedTimer[] = [];
|
|
71
|
+
|
|
72
|
+
const _prevSetTimeout = globalThis.setTimeout;
|
|
73
|
+
const _prevClearTimeout = globalThis.clearTimeout;
|
|
74
|
+
|
|
75
|
+
(globalThis as any).setTimeout = function(fn: any, delay?: number, ...args: any[]) {
|
|
76
|
+
if (typeof fn !== 'function') {
|
|
77
|
+
return (_prevSetTimeout as any)(fn, delay, ...args);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const entry: TrackedTimer = { fn, args, cleared: false, realId: null };
|
|
81
|
+
|
|
82
|
+
entry.realId = (_prevSetTimeout as any)((...a: any[]) => {
|
|
83
|
+
if (!entry.cleared) {
|
|
84
|
+
entry.cleared = true;
|
|
85
|
+
fn(...a);
|
|
86
|
+
}
|
|
87
|
+
}, delay, ...args);
|
|
88
|
+
|
|
89
|
+
_trackedTimers.push(entry);
|
|
90
|
+
return entry.realId;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
(globalThis as any).clearTimeout = function(id: any) {
|
|
94
|
+
for (const entry of _trackedTimers) {
|
|
95
|
+
if (entry.realId === id || (entry.realId && typeof entry.realId === 'object' &&
|
|
96
|
+
id && typeof id === 'object' && entry.realId[Symbol.toPrimitive]?.() === id[Symbol.toPrimitive]?.())) {
|
|
97
|
+
entry.cleared = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return (_prevClearTimeout as any)(id);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function flushTrackedTimers() {
|
|
104
|
+
const pending = _trackedTimers.splice(0);
|
|
105
|
+
|
|
106
|
+
const origSetHas = Set.prototype.has;
|
|
107
|
+
if (_cfQueue) {
|
|
108
|
+
Set.prototype.has = function() { return false; };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const entry of pending) {
|
|
112
|
+
if (!entry.cleared) {
|
|
113
|
+
entry.cleared = true;
|
|
114
|
+
try { (_prevClearTimeout as any)(entry.realId); } catch (_e) { /* ignore */ }
|
|
115
|
+
try { entry.fn(...entry.args); } catch (e) { console.error('[queue] timer flush error:', e); }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (_cfQueue) {
|
|
120
|
+
Set.prototype.has = origSetHas;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function flushPendingJobs() {
|
|
125
|
+
const MAX_ITERATIONS = 10;
|
|
126
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
127
|
+
flushTrackedTimers();
|
|
128
|
+
|
|
129
|
+
if (_pendingPushJobs.length === 0) break;
|
|
130
|
+
const batch = _pendingPushJobs.splice(0);
|
|
131
|
+
await Promise.allSettled(batch);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Scheduled jobs: dispatch due jobs from D1 to CF Queue ---
|
|
136
|
+
type ScheduledQueueEntry = {
|
|
137
|
+
store: ReturnType<typeof createQueueStore>;
|
|
138
|
+
executeJob: (jobId: string, job: any, persist: boolean) => Promise<any>;
|
|
139
|
+
cancel: (id: string) => Promise<any>;
|
|
140
|
+
name: string;
|
|
141
|
+
};
|
|
142
|
+
const _scheduledQueues = new Map<string, ScheduledQueueEntry>();
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Dispatch all due delayed jobs from D1 to CF Queue for execution.
|
|
146
|
+
* Called from CF Cron Trigger (every minute).
|
|
147
|
+
*
|
|
148
|
+
* This replaces the original loop() that polled every 4 seconds.
|
|
149
|
+
* Jobs with will_run_at <= now are picked up, cancelled in D1 (to prevent
|
|
150
|
+
* re-dispatch next cycle), then sent to CF Queue for execution.
|
|
151
|
+
* If CF Queue is unavailable, execute inline as fallback.
|
|
152
|
+
*/
|
|
153
|
+
export async function runAllScheduledJobs(): Promise<{ dispatched: number; failed: number; queues: string[] }> {
|
|
154
|
+
let totalDispatched = 0;
|
|
155
|
+
let totalFailed = 0;
|
|
156
|
+
const queuesProcessed: string[] = [];
|
|
157
|
+
|
|
158
|
+
// Dispatch helper: send job to CF Queue, or execute inline if unavailable
|
|
159
|
+
//
|
|
160
|
+
// Aligns with the original Blocklet Server semantics: the scheduler removes the
|
|
161
|
+
// D1 row BEFORE onJob runs, so fire-and-forget re-pushes inside onJob (e.g.
|
|
162
|
+
// handlePayment → paymentQueue.push({ runAt }) after a payment failure) can
|
|
163
|
+
// freely INSERT a new scheduled row with the same id without hitting the
|
|
164
|
+
// store.addJob duplicate check.
|
|
165
|
+
//
|
|
166
|
+
// The message carries persist:false so the consumer does NOT delete the row
|
|
167
|
+
// again after onJob completes — otherwise it would wipe out the freshly
|
|
168
|
+
// re-scheduled row inserted by the running handler.
|
|
169
|
+
const dispatchJob = async (
|
|
170
|
+
queueName: string,
|
|
171
|
+
entry: { executeJob: Function; cancel: Function; store: ReturnType<typeof createQueueStore> },
|
|
172
|
+
jobRecord: any,
|
|
173
|
+
) => {
|
|
174
|
+
// Delete the old row up-front. Match the original loop() invariant:
|
|
175
|
+
// once a scheduled row is picked up, it should no longer exist as a
|
|
176
|
+
// "pending scheduled" row in D1 — the handler is now responsible for
|
|
177
|
+
// its fate (success → nothing to clean up; failure → re-push writes a
|
|
178
|
+
// brand new row).
|
|
179
|
+
try {
|
|
180
|
+
await entry.store.deleteJob(jobRecord.id);
|
|
181
|
+
} catch (_e) {
|
|
182
|
+
/* ignore — best effort */
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (_cfQueue) {
|
|
186
|
+
try {
|
|
187
|
+
await _cfQueue.send({ queueName, jobId: jobRecord.id, job: jobRecord.job, persist: false });
|
|
188
|
+
} catch (sendErr: any) {
|
|
189
|
+
// CF Queue unavailable → run inline with persist:false
|
|
190
|
+
// (row was already deleted above; don't try to delete again)
|
|
191
|
+
await entry.executeJob(jobRecord.id, jobRecord.job, false);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
await entry.executeJob(jobRecord.id, jobRecord.job, false);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Single aggregated D1 query across ALL registered queues. Replaces the
|
|
199
|
+
// previous per-queue loop (16 queues × 2 queries = 32 serial round-trips,
|
|
200
|
+
// ~2s wall time per scheduled tick) that consistently tripped CF Workers'
|
|
201
|
+
// scheduled event time budget and produced ~1,440 exceededCpu outcomes/day.
|
|
202
|
+
//
|
|
203
|
+
// Shape: one findAll covering
|
|
204
|
+
// (a) delayed jobs that are due — delay != -1 AND will_run_at <= now
|
|
205
|
+
// (b) immediate jobs stuck in D1 — delay == -1 AND created_at < now-2min
|
|
206
|
+
// Results are grouped by queue name and dispatched via the existing
|
|
207
|
+
// per-queue dispatchJob path, preserving original behavior.
|
|
208
|
+
const queueNames = Array.from(_handlers.keys());
|
|
209
|
+
if (queueNames.length === 0) {
|
|
210
|
+
return { dispatched: 0, failed: 0, queues: [] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
// SQLite / D1 stores DATE columns as ISO strings. The raw D1 bind layer
|
|
215
|
+
// rejects Date objects with "Type 'object' not supported", so we pass the
|
|
216
|
+
// serialized string here instead of a native Date.
|
|
217
|
+
const twoMinAgoISO = new Date(now - 2 * 60 * 1000).toISOString();
|
|
218
|
+
|
|
219
|
+
let allJobs: TJob[] = [];
|
|
220
|
+
try {
|
|
221
|
+
allJobs = (await Job.findAll({
|
|
222
|
+
where: {
|
|
223
|
+
queue: { [Op.in]: queueNames },
|
|
224
|
+
cancelled: false,
|
|
225
|
+
[Op.or]: [
|
|
226
|
+
{ delay: { [Op.not]: -1 }, will_run_at: { [Op.lte]: now } },
|
|
227
|
+
{ delay: -1, created_at: { [Op.lt]: twoMinAgoISO } },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
order: [['created_at', 'ASC']],
|
|
231
|
+
transaction: null,
|
|
232
|
+
})) as unknown as TJob[];
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
console.error('[queue:dispatch] Aggregated D1 scan failed:', err?.message || err);
|
|
235
|
+
return { dispatched: 0, failed: 0, queues: [] };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (allJobs.length === 0) {
|
|
239
|
+
return { dispatched: 0, failed: 0, queues: [] };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Group by queue, preserving created_at ASC order from the SQL ORDER BY.
|
|
243
|
+
const jobsByQueue = new Map<string, TJob[]>();
|
|
244
|
+
for (const job of allJobs) {
|
|
245
|
+
if (!job.job || !job.id) continue;
|
|
246
|
+
const arr = jobsByQueue.get(job.queue) ?? [];
|
|
247
|
+
arr.push(job);
|
|
248
|
+
jobsByQueue.set(job.queue, arr);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const [queueName, jobs] of jobsByQueue) {
|
|
252
|
+
const entry = _handlers.get(queueName);
|
|
253
|
+
if (!entry || jobs.length === 0) continue;
|
|
254
|
+
|
|
255
|
+
queuesProcessed.push(queueName);
|
|
256
|
+
|
|
257
|
+
for (const jobRecord of jobs) {
|
|
258
|
+
try {
|
|
259
|
+
await dispatchJob(queueName, entry, jobRecord);
|
|
260
|
+
totalDispatched++;
|
|
261
|
+
} catch (err: any) {
|
|
262
|
+
totalFailed++;
|
|
263
|
+
console.error(`[queue:dispatch] Failed ${queueName}/${jobRecord.id}:`, err?.message || err);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { dispatched: totalDispatched, failed: totalFailed, queues: queuesProcessed };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Queue factory ---
|
|
272
|
+
|
|
273
|
+
type QueueOptions<T> = {
|
|
274
|
+
id?: (job: T) => string;
|
|
275
|
+
concurrency?: number;
|
|
276
|
+
maxRetries?: number;
|
|
277
|
+
maxTimeout?: number;
|
|
278
|
+
retryDelay?: number;
|
|
279
|
+
enableScheduledJob?: boolean;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
type QueueParams<T> = {
|
|
283
|
+
name: string;
|
|
284
|
+
onJob: (job: T) => Promise<any>;
|
|
285
|
+
options?: QueueOptions<T>;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const defaults: QueueOptions<any> = {
|
|
289
|
+
concurrency: 1,
|
|
290
|
+
maxRetries: 1,
|
|
291
|
+
maxTimeout: 24 * 60 * 60 * 1000,
|
|
292
|
+
retryDelay: 0,
|
|
293
|
+
enableScheduledJob: false,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
type PushParams<T> = {
|
|
297
|
+
job: T;
|
|
298
|
+
id?: string;
|
|
299
|
+
persist?: boolean;
|
|
300
|
+
delay?: number;
|
|
301
|
+
runAt?: number;
|
|
302
|
+
skipDuplicateCheck?: boolean;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export default function createQueue<T = any>({ name, onJob, options = defaults }: QueueParams<T>) {
|
|
306
|
+
const store = createQueueStore(name);
|
|
307
|
+
const { concurrency, maxRetries, maxTimeout, retryDelay, enableScheduledJob } = Object.assign({}, defaults, options);
|
|
308
|
+
|
|
309
|
+
const getJobId = (id: string | undefined, job: any) => id || (options?.id ? options.id(job) : nanoid()) || nanoid();
|
|
310
|
+
|
|
311
|
+
const queueEvents = new EventEmitter();
|
|
312
|
+
|
|
313
|
+
// Execute the job directly (used by pushAndWait and queue consumer)
|
|
314
|
+
const executeJob = async (jobId: string, job: T, persist: boolean): Promise<{ id: string; job: T; result: any }> => {
|
|
315
|
+
let retryCount = 0;
|
|
316
|
+
|
|
317
|
+
// eslint-disable-next-line no-constant-condition
|
|
318
|
+
while (true) {
|
|
319
|
+
try {
|
|
320
|
+
console.log(`[queue:${name}] executing job`, jobId);
|
|
321
|
+
const result = await onJob(job);
|
|
322
|
+
console.log(`[queue:${name}] job finished`, jobId);
|
|
323
|
+
|
|
324
|
+
if (persist) {
|
|
325
|
+
try { await store.deleteJob(jobId); } catch (_e) { /* ignore */ }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { id: jobId, job, result };
|
|
329
|
+
} catch (err: any) {
|
|
330
|
+
console.error(`[queue:${name}] job error`, jobId, err?.message || err);
|
|
331
|
+
|
|
332
|
+
if (err?.nonRetryable === true) {
|
|
333
|
+
if (persist) {
|
|
334
|
+
try { await store.deleteJob(jobId); } catch (_e) { /* ignore */ }
|
|
335
|
+
}
|
|
336
|
+
throw err;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
retryCount++;
|
|
340
|
+
if (retryCount >= maxRetries!) {
|
|
341
|
+
if (persist) {
|
|
342
|
+
try { await store.deleteJob(jobId); } catch (_e) { /* ignore */ }
|
|
343
|
+
}
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log(`[queue:${name}] retrying job`, jobId, `attempt ${retryCount}/${maxRetries}`);
|
|
348
|
+
queueEvents.emit('retry', { id: jobId, job });
|
|
349
|
+
if (retryDelay && retryDelay > 0) {
|
|
350
|
+
await new Promise((r) => (_prevSetTimeout as any)(r, retryDelay));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Send job to CF Queue for execution (replaces fastq.push)
|
|
357
|
+
const sendToCFQueue = async (jobId: string, job: T) => {
|
|
358
|
+
const message = { queueName: name, jobId, job };
|
|
359
|
+
await _cfQueue.send(message);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const push = ({ job, id, persist = true, delay, runAt, skipDuplicateCheck = false }: PushParams<T>) => {
|
|
363
|
+
const jobEvents = new EventEmitter();
|
|
364
|
+
const emit = (e: string, data: any) => {
|
|
365
|
+
queueEvents.emit(e, data);
|
|
366
|
+
jobEvents.emit(e, data);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
if (!job) {
|
|
370
|
+
throw new Error('Can not queue empty job');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const jobId = getJobId(id, job);
|
|
374
|
+
|
|
375
|
+
// Calculate delay in seconds
|
|
376
|
+
let delaySeconds = 0;
|
|
377
|
+
if (delay) {
|
|
378
|
+
delaySeconds = delay;
|
|
379
|
+
} else if (runAt) {
|
|
380
|
+
delaySeconds = Math.max(0, Math.floor((runAt as number) - Date.now() / 1000));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const isImmediate = delaySeconds <= 0;
|
|
384
|
+
|
|
385
|
+
const enqueue = async () => {
|
|
386
|
+
if (isImmediate) {
|
|
387
|
+
// === Immediate job ===
|
|
388
|
+
// 1. Persist to D1 (source of truth)
|
|
389
|
+
// 2. Send to CF Queue for execution (like fastq.push)
|
|
390
|
+
// 3. If CF Queue fails → job stays in D1, cron will dispatch
|
|
391
|
+
if (persist) {
|
|
392
|
+
try {
|
|
393
|
+
await store.addJob(jobId, job, {}, skipDuplicateCheck);
|
|
394
|
+
} catch (err: any) {
|
|
395
|
+
const isDuplicate = err?.code === 'JOB_DUPLICATE' ||
|
|
396
|
+
err?.message?.includes('UNIQUE constraint failed') ||
|
|
397
|
+
err?.message?.includes('SQLITE_CONSTRAINT');
|
|
398
|
+
if (isDuplicate) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
console.error(`[queue:${name}] addJob error`, err?.message);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
emit('queued', { id: jobId, job, persist });
|
|
407
|
+
|
|
408
|
+
if (_cfQueue) {
|
|
409
|
+
try {
|
|
410
|
+
await sendToCFQueue(jobId, job);
|
|
411
|
+
} catch (_err: any) {
|
|
412
|
+
// CF Queue unavailable — job is safe in D1, cron will dispatch
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
// No CF Queue binding — execute inline (Blocklet Server compatibility)
|
|
416
|
+
try {
|
|
417
|
+
const data = await executeJob(jobId, job, persist);
|
|
418
|
+
emit('finished', data);
|
|
419
|
+
} catch (err: any) {
|
|
420
|
+
emit('failed', { id: jobId, job, error: err });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
// === Delayed job ===
|
|
425
|
+
// Only persist to D1 with will_run_at. Cron dispatches when due.
|
|
426
|
+
// NOT sent to CF Queue — allows cancel/replace via D1 delete.
|
|
427
|
+
if (persist) {
|
|
428
|
+
const attrs: any = { delay: delaySeconds, will_run_at: Date.now() + delaySeconds * 1000 };
|
|
429
|
+
try {
|
|
430
|
+
await store.addJob(jobId, job, attrs, skipDuplicateCheck);
|
|
431
|
+
} catch (err: any) {
|
|
432
|
+
const isDuplicate = err?.code === 'JOB_DUPLICATE' ||
|
|
433
|
+
err?.message?.includes('UNIQUE constraint failed') ||
|
|
434
|
+
err?.message?.includes('SQLITE_CONSTRAINT');
|
|
435
|
+
if (isDuplicate) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
console.error(`[queue:${name}] addJob error for delayed job`, err?.message);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
emit('queued', { id: jobId, job, persist });
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const promise = enqueue()
|
|
447
|
+
.catch((err) => console.error(`[queue:${name}] push error:`, err?.message || err));
|
|
448
|
+
|
|
449
|
+
// Register promise to keep Worker alive
|
|
450
|
+
const isHttp = (globalThis as any).__cfHttpContext__;
|
|
451
|
+
if (isHttp && _waitUntil) {
|
|
452
|
+
_waitUntil(promise);
|
|
453
|
+
} else {
|
|
454
|
+
_pendingPushJobs.push(promise);
|
|
455
|
+
if (_waitUntil) {
|
|
456
|
+
_waitUntil(promise);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
(jobEvents as any).id = jobId;
|
|
461
|
+
return jobEvents;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// pushAndWait always runs inline (caller needs the result synchronously)
|
|
465
|
+
const pushAndWait = async (params: PushParams<T>) => {
|
|
466
|
+
if (!params.job) {
|
|
467
|
+
throw new Error('Can not queue empty job');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const jobId = getJobId(params.id, params.job);
|
|
471
|
+
const persist = params.persist !== false;
|
|
472
|
+
|
|
473
|
+
if (persist) {
|
|
474
|
+
try {
|
|
475
|
+
await store.addJob(jobId, params.job, {}, params.skipDuplicateCheck || false);
|
|
476
|
+
} catch (err: any) {
|
|
477
|
+
const isDuplicate = err?.code === 'JOB_DUPLICATE' ||
|
|
478
|
+
err?.message?.includes('UNIQUE constraint failed') ||
|
|
479
|
+
err?.message?.includes('SQLITE_CONSTRAINT');
|
|
480
|
+
if (isDuplicate) {
|
|
481
|
+
console.log(`[queue:${name}] pushAndWait duplicate job`, jobId);
|
|
482
|
+
} else {
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const data = await executeJob(jobId, params.job, persist);
|
|
490
|
+
queueEvents.emit('finished', data);
|
|
491
|
+
return data;
|
|
492
|
+
} catch (err: any) {
|
|
493
|
+
queueEvents.emit('failed', { id: jobId, job: params.job, error: err });
|
|
494
|
+
throw { id: jobId, job: params.job, error: err };
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const cancel = async (id: string) => {
|
|
499
|
+
try {
|
|
500
|
+
const doc = await store.updateJob(id, { cancelled: true });
|
|
501
|
+
return doc ? doc.job : null;
|
|
502
|
+
} catch (_e) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const getJob = async (id: string) => {
|
|
508
|
+
try {
|
|
509
|
+
const doc = await store.getJob(id);
|
|
510
|
+
return doc ? doc.job : null;
|
|
511
|
+
} catch (_e) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const deleteJob = async (id: string, knownExists: boolean = false): Promise<boolean> => {
|
|
517
|
+
if (!knownExists) {
|
|
518
|
+
const exists = await getJob(id);
|
|
519
|
+
if (!exists) return false;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
await store.deleteJob(id);
|
|
523
|
+
return true;
|
|
524
|
+
} catch (_e) {
|
|
525
|
+
try { await cancel(id); } catch (_e2) { /* ignore */ }
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const updateJob = async (id: string, updates: any) => {
|
|
531
|
+
return store.updateJob(id, updates);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const queueInstance = Object.assign(queueEvents, {
|
|
535
|
+
store,
|
|
536
|
+
push,
|
|
537
|
+
pushAndWait,
|
|
538
|
+
drain: (_cb: any) => {},
|
|
539
|
+
empty: (_cb: any) => {},
|
|
540
|
+
saturated: (_cb: any) => {},
|
|
541
|
+
error: (_cb: any) => {},
|
|
542
|
+
get: getJob,
|
|
543
|
+
delete: deleteJob,
|
|
544
|
+
cancel,
|
|
545
|
+
update: updateJob,
|
|
546
|
+
executeJob,
|
|
547
|
+
options: {
|
|
548
|
+
concurrency,
|
|
549
|
+
maxRetries,
|
|
550
|
+
maxTimeout,
|
|
551
|
+
retryDelay,
|
|
552
|
+
enableScheduledJob,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Register handler for CF Queue consumer dispatch
|
|
557
|
+
_handlers.set(name, { onJob, executeJob, store, cancel });
|
|
558
|
+
|
|
559
|
+
// Register for scheduled job dispatch (cron picks up due delayed jobs)
|
|
560
|
+
if (enableScheduledJob) {
|
|
561
|
+
_scheduledQueues.set(name, { store, executeJob, cancel, name });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return queueInstance;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Exported for unit tests; not part of the public queue API.
|
|
568
|
+
export const __test__ = {
|
|
569
|
+
resetHandlers() {
|
|
570
|
+
_handlers.clear();
|
|
571
|
+
_scheduledQueues.clear();
|
|
572
|
+
_pendingPushJobs.length = 0;
|
|
573
|
+
_trackedTimers.length = 0;
|
|
574
|
+
},
|
|
575
|
+
registerForTest(name: string, overrides: Partial<RegisteredHandler>) {
|
|
576
|
+
const noop = async () => undefined;
|
|
577
|
+
const store = createQueueStore(name);
|
|
578
|
+
_handlers.set(name, {
|
|
579
|
+
onJob: overrides.onJob ?? noop,
|
|
580
|
+
executeJob: overrides.executeJob ?? (noop as any),
|
|
581
|
+
cancel: overrides.cancel ?? (noop as any),
|
|
582
|
+
store: overrides.store ?? store,
|
|
583
|
+
});
|
|
584
|
+
},
|
|
585
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Shim for */esm/_virtual/rolldown_runtime.mjs
|
|
2
|
+
// CF Workers bundle doesn't support createRequire(import.meta.url).
|
|
3
|
+
// We statically import modules that __require() needs and serve them.
|
|
4
|
+
|
|
5
|
+
import * as googleProtobuf from 'google-protobuf';
|
|
6
|
+
import * as googleProtobufAnyPb from 'google-protobuf/google/protobuf/any_pb';
|
|
7
|
+
import * as googleProtobufTimestampPb from 'google-protobuf/google/protobuf/timestamp_pb';
|
|
8
|
+
import _debugImport from 'debug';
|
|
9
|
+
|
|
10
|
+
const debugFactory = typeof _debugImport === 'function' ? _debugImport : (_debugImport as any).default || (() => () => {});
|
|
11
|
+
|
|
12
|
+
const _modules: Record<string, any> = {
|
|
13
|
+
'google-protobuf': googleProtobuf,
|
|
14
|
+
'google-protobuf/google/protobuf/any_pb': googleProtobufAnyPb,
|
|
15
|
+
'google-protobuf/google/protobuf/timestamp_pb': googleProtobufTimestampPb,
|
|
16
|
+
'debug': debugFactory,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const __commonJSMin = (cb: any, mod?: any) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
20
|
+
|
|
21
|
+
const __require = (mod: string) => {
|
|
22
|
+
if (_modules[mod]) return _modules[mod];
|
|
23
|
+
if (typeof globalThis.require === 'function') {
|
|
24
|
+
try { return globalThis.require(mod); } catch {}
|
|
25
|
+
}
|
|
26
|
+
console.warn('[rolldown-runtime] Cannot require:', mod);
|
|
27
|
+
return {};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Matches rolldown's __exportAll(gettersMap, symbols?) → frozen module object
|
|
31
|
+
const __defProp = Object.defineProperty;
|
|
32
|
+
const __exportAll = (all: Record<string, () => any>, symbols?: boolean) => {
|
|
33
|
+
const target: any = {};
|
|
34
|
+
for (const name in all) {
|
|
35
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
36
|
+
}
|
|
37
|
+
if (symbols) {
|
|
38
|
+
__defProp(target, Symbol.toStringTag, { value: 'Module' });
|
|
39
|
+
}
|
|
40
|
+
return target;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { __commonJSMin, __require, __exportAll };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// DataTypes stub — used in Model.init() for schema registration
|
|
2
|
+
// In D1 shim, we collect these to know column types but don't create tables at runtime
|
|
3
|
+
// (tables are created via D1 migrations)
|
|
4
|
+
|
|
5
|
+
export const DataTypes = {
|
|
6
|
+
STRING: (len?: number) => ({ type: 'TEXT', length: len }),
|
|
7
|
+
TEXT: { type: 'TEXT' },
|
|
8
|
+
INTEGER: { type: 'INTEGER' },
|
|
9
|
+
BIGINT: { type: 'INTEGER' },
|
|
10
|
+
FLOAT: { type: 'REAL' },
|
|
11
|
+
DOUBLE: { type: 'REAL' },
|
|
12
|
+
BOOLEAN: { type: 'INTEGER' },
|
|
13
|
+
DATE: { type: 'TEXT' },
|
|
14
|
+
DATEONLY: { type: 'TEXT' },
|
|
15
|
+
JSON: { type: 'TEXT' },
|
|
16
|
+
JSONB: { type: 'TEXT' },
|
|
17
|
+
DECIMAL: (precision?: number, scale?: number) => ({ type: 'REAL', precision, scale }),
|
|
18
|
+
BLOB: { type: 'BLOB' },
|
|
19
|
+
ENUM: (...values: string[]) => ({ type: 'TEXT', values }),
|
|
20
|
+
ARRAY: (itemType: any) => ({ type: 'TEXT', itemType }),
|
|
21
|
+
VIRTUAL: { type: 'VIRTUAL' },
|
|
22
|
+
UUID: { type: 'TEXT' },
|
|
23
|
+
NOW: 'CF_NOW_PLACEHOLDER',
|
|
24
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Sequelize helper functions: literal(), fn(), col()
|
|
2
|
+
// Used sparingly in Payment Kit (~7 places)
|
|
3
|
+
|
|
4
|
+
export function literal(sql: string) {
|
|
5
|
+
return { __literal: true, sql };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function fn(name: string, ...args: any[]) {
|
|
9
|
+
return { __fn: true, name, args };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function col(name: string) {
|
|
13
|
+
return { __col: true, name };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Convert helpers to SQL string
|
|
17
|
+
export function helperToSQL(value: any): string {
|
|
18
|
+
if (!value || typeof value !== 'object') return String(value);
|
|
19
|
+
if (value.__literal) return value.sql;
|
|
20
|
+
if (value.__col) {
|
|
21
|
+
// Handle dotted col names
|
|
22
|
+
if (value.name.includes('.')) return `"${value.name.replace('.', '"."')}"`;
|
|
23
|
+
return `"${value.name}"`;
|
|
24
|
+
}
|
|
25
|
+
if (value.__fn) {
|
|
26
|
+
const args = value.args.map((a: any) => {
|
|
27
|
+
if (a === null || a === undefined) return 'NULL';
|
|
28
|
+
if (typeof a === 'string') {
|
|
29
|
+
// String args in fn() are usually column names (e.g., fn('COUNT', col('id')))
|
|
30
|
+
// But literal strings passed to fn should be quoted differently
|
|
31
|
+
// Convention: use col() for column refs; bare strings are treated as column names too
|
|
32
|
+
return `"${a}"`;
|
|
33
|
+
}
|
|
34
|
+
if (typeof a === 'number') return String(a);
|
|
35
|
+
return helperToSQL(a);
|
|
36
|
+
});
|
|
37
|
+
return `${value.name}(${args.join(', ')})`;
|
|
38
|
+
}
|
|
39
|
+
// Sequelize.where() wrapper
|
|
40
|
+
if (value.__sequelizeWhere) {
|
|
41
|
+
const lhsSQL = helperToSQL(value.lhs);
|
|
42
|
+
const rhsSQL = value.rhs === null ? 'NULL' : '?';
|
|
43
|
+
return `${lhsSQL} = ${rhsSQL}`;
|
|
44
|
+
}
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|