@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc

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.
Files changed (224) hide show
  1. package/README.md +27 -2
  2. package/dist/GCSStorageController-fv6JAZ87.js +402 -0
  3. package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
  4. package/dist/S3StorageController-BNg-pXiY.js +480 -0
  5. package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
  6. package/dist/api/mount-path.d.ts +30 -0
  7. package/dist/api/mount.d.ts +6 -13
  8. package/dist/api/rest/api-generator.d.ts +53 -1
  9. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  10. package/dist/api/rest/field-access-query.d.ts +28 -8
  11. package/dist/api/rest/field-ops.d.ts +19 -7
  12. package/dist/api/rest/query-body.d.ts +10 -0
  13. package/dist/api/rest/write-validation.d.ts +7 -5
  14. package/dist/api/schema-editor-routes.d.ts +10 -1
  15. package/dist/api/sql-export-routes.d.ts +8 -0
  16. package/dist/api/types.d.ts +35 -0
  17. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
  18. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
  19. package/dist/auth/access.d.ts +57 -8
  20. package/dist/auth/admin-user-ops.d.ts +24 -2
  21. package/dist/auth/admin-users-route.d.ts +1 -0
  22. package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
  23. package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
  24. package/dist/auth/builtin-auth-adapter.d.ts +13 -0
  25. package/dist/auth/email-change-routes.d.ts +3 -0
  26. package/dist/auth/expired-token-sweep.d.ts +5 -1
  27. package/dist/auth/interfaces.d.ts +10 -0
  28. package/dist/auth/jwks-routes.d.ts +5 -1
  29. package/dist/auth/jwt-crypto.d.ts +7 -0
  30. package/dist/auth/jwt-keys.d.ts +4 -0
  31. package/dist/auth/jwt.d.ts +282 -166
  32. package/dist/auth/magic-link-routes.d.ts +3 -0
  33. package/dist/auth/mfa-crypto.d.ts +9 -3
  34. package/dist/auth/mfa-gate.d.ts +4 -2
  35. package/dist/auth/mfa-routes.d.ts +6 -0
  36. package/dist/auth/middleware.d.ts +9 -2
  37. package/dist/auth/otp-routes.d.ts +3 -0
  38. package/dist/auth/rate-limiter.d.ts +61 -34
  39. package/dist/auth/registration-policy.d.ts +17 -0
  40. package/dist/auth/require-auth.d.ts +1 -1
  41. package/dist/auth/routes.d.ts +16 -0
  42. package/dist/auth/session-routes.d.ts +6 -0
  43. package/dist/auth/user-picking.d.ts +37 -0
  44. package/dist/auth/verify-credential.d.ts +25 -4
  45. package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
  46. package/dist/auth-GnONzteu.js.map +1 -0
  47. package/dist/auth_adapter-CIItCTc8.js +21 -0
  48. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  49. package/dist/backend/create-backend.d.ts +281 -0
  50. package/dist/backend/default-backend.d.ts +35 -0
  51. package/dist/backend/options.d.ts +912 -0
  52. package/dist/backend/scope.d.ts +23 -0
  53. package/dist/backend/services.d.ts +54 -0
  54. package/dist/backend-DTAOsLQc.js.map +1 -1
  55. package/dist/backup/backup-common.d.ts +14 -3
  56. package/dist/backup/backup-routes.d.ts +23 -2
  57. package/dist/backup/backup-storage.d.ts +3 -1
  58. package/dist/backup/index.d.ts +3 -1
  59. package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
  60. package/dist/backup-BnvkSLHi.js.map +1 -0
  61. package/dist/boot/boot.d.ts +2 -2
  62. package/dist/boot/driver.d.ts +3 -0
  63. package/dist/boot/env.d.ts +9 -2
  64. package/dist/boot/http-server.d.ts +22 -0
  65. package/dist/boot/options.d.ts +16 -1
  66. package/dist/boot/provision.d.ts +4 -2
  67. package/dist/boot/runtime-cors.d.ts +17 -0
  68. package/dist/boot/sources.d.ts +2 -0
  69. package/dist/boot/static-routing.d.ts +9 -0
  70. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
  71. package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
  72. package/dist/cron/cron-loader.d.ts +77 -18
  73. package/dist/cron/cron-routes.d.ts +1 -1
  74. package/dist/cron/cron-scheduler.d.ts +69 -3
  75. package/dist/cron/cron-store.d.ts +33 -5
  76. package/dist/cron/define-cron.d.ts +6 -2
  77. package/dist/cron/index.d.ts +3 -3
  78. package/dist/cron-loader-B-u0snqq.js +202 -0
  79. package/dist/cron-loader-B-u0snqq.js.map +1 -0
  80. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
  81. package/dist/cron-routes-OD6BstUj.js.map +1 -0
  82. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
  83. package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
  84. package/dist/cron-store-CZ30ZBi-.js +577 -0
  85. package/dist/cron-store-CZ30ZBi-.js.map +1 -0
  86. package/dist/email/smtp-email-service.d.ts +23 -0
  87. package/dist/email/types.d.ts +18 -0
  88. package/dist/env.d.ts +0 -37
  89. package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
  90. package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
  91. package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
  92. package/dist/function-loader-iKhOZatS.js.map +1 -0
  93. package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
  94. package/dist/function-routes-j9TwpvQe.js.map +1 -0
  95. package/dist/functions/context.d.ts +9 -0
  96. package/dist/functions/define-function.d.ts +10 -6
  97. package/dist/functions/function-loader.d.ts +75 -18
  98. package/dist/functions/function-routes.d.ts +2 -1
  99. package/dist/functions/index.d.ts +5 -4
  100. package/dist/functions/index.js +42 -13
  101. package/dist/functions/index.js.map +1 -1
  102. package/dist/functions/internal.d.ts +2 -2
  103. package/dist/functions/proxy.d.ts +6 -2
  104. package/dist/functions/request-timeout.d.ts +7 -5
  105. package/dist/history/history-routes.d.ts +9 -0
  106. package/dist/history/retention-sweep.d.ts +81 -0
  107. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
  108. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
  109. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  110. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  111. package/dist/index.d.ts +23 -10
  112. package/dist/index.es.js +24423 -20038
  113. package/dist/index.es.js.map +1 -1
  114. package/dist/init/docs.d.ts +27 -10
  115. package/dist/init/process-safety.d.ts +7 -2
  116. package/dist/init/shutdown.d.ts +18 -3
  117. package/dist/init.d.ts +49 -865
  118. package/dist/internal-tables-L2t39RdT.js +136 -0
  119. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  120. package/dist/jobs/job-queue.d.ts +8 -0
  121. package/dist/jobs/job-store.d.ts +24 -0
  122. package/dist/jobs/types.d.ts +25 -4
  123. package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
  124. package/dist/jobs-BCbflZj2.js.map +1 -0
  125. package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
  126. package/dist/jwt-BlvuKoR6.js.map +1 -0
  127. package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
  128. package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
  129. package/dist/load-failure-L5hvfDoG.js +80 -0
  130. package/dist/load-failure-L5hvfDoG.js.map +1 -0
  131. package/dist/mcp/mcp-routes.d.ts +6 -0
  132. package/dist/mcp/mcp-tools.d.ts +8 -0
  133. package/dist/mcp/oauth-routes.d.ts +7 -0
  134. package/dist/metrics/index.d.ts +18 -0
  135. package/dist/metrics/pool-wait.d.ts +50 -0
  136. package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
  137. package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
  138. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
  139. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
  140. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
  141. package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
  142. package/dist/policy-PdzX-Opd.js +121 -0
  143. package/dist/policy-PdzX-Opd.js.map +1 -0
  144. package/dist/project_manifest-R6y_KHms.js +59 -0
  145. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  146. package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
  147. package/dist/proxy-CkTlMGmh.js.map +1 -0
  148. package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
  149. package/dist/query-parser-4WkMoA_K.js.map +1 -0
  150. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  151. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  152. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  153. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  154. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  155. package/dist/schema-edit/local-source.d.ts +29 -0
  156. package/dist/schema-edit/remote-source.d.ts +13 -3
  157. package/dist/schema-edit/source-editor.d.ts +28 -0
  158. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
  159. package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
  160. package/dist/services/outbound-post.d.ts +50 -0
  161. package/dist/services/outbound-url-guard.d.ts +34 -5
  162. package/dist/services/routed-realtime-service.d.ts +5 -0
  163. package/dist/services/webhook-service.d.ts +66 -14
  164. package/dist/singleton.d.ts +9 -5
  165. package/dist/soft-delete-params-DfV3vVN5.js +668 -0
  166. package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
  167. package/dist/sql-export-routes-ClRwzIsC.js +240 -0
  168. package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
  169. package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
  170. package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
  171. package/dist/storage/GCSStorageController.d.ts +46 -2
  172. package/dist/storage/LocalStorageController.d.ts +67 -2
  173. package/dist/storage/S3StorageController.d.ts +37 -3
  174. package/dist/storage/image-transform.d.ts +5 -0
  175. package/dist/storage/maintenance-routes.d.ts +23 -0
  176. package/dist/storage/maintenance.d.ts +126 -0
  177. package/dist/storage/rendition-cache.d.ts +9 -11
  178. package/dist/storage/request-principals.d.ts +30 -0
  179. package/dist/storage/routes.d.ts +20 -0
  180. package/dist/storage/source-variables.d.ts +10 -0
  181. package/dist/storage/streams.d.ts +42 -0
  182. package/dist/storage/tus-handler.d.ts +46 -2
  183. package/dist/storage/types.d.ts +82 -7
  184. package/dist/storage/upload-validation.d.ts +13 -0
  185. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  186. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  187. package/dist/strings.es-BeZWAodn.js +256 -0
  188. package/dist/strings.es-BeZWAodn.js.map +1 -0
  189. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  190. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  191. package/dist/utils/byte-size.d.ts +10 -0
  192. package/dist/utils/compression.d.ts +6 -0
  193. package/dist/utils/load-failure.d.ts +14 -0
  194. package/package.json +5 -5
  195. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  196. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  197. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  198. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  199. package/dist/auth-DMLngxn_.js.map +0 -1
  200. package/dist/backup-CN0s50D2.js.map +0 -1
  201. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  202. package/dist/cron-loader-CwaANlOG.js +0 -118
  203. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  204. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  205. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  206. package/dist/cron-store-D2Q9-Aco.js +0 -300
  207. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  208. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  209. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  210. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  211. package/dist/function-routes-CaNG4waN.js.map +0 -1
  212. package/dist/jobs-DqYNfquG.js.map +0 -1
  213. package/dist/jwt-R6bSPMjk.js.map +0 -1
  214. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  215. package/dist/proxy-qRlqeUmO.js.map +0 -1
  216. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  217. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  218. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  219. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  220. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  221. package/dist/src-I3aG1PcY.js +0 -6870
  222. package/dist/src-I3aG1PcY.js.map +0 -1
  223. package/dist/types-BfKcm9do.js +0 -39
  224. package/dist/types-BfKcm9do.js.map +0 -1
@@ -0,0 +1,577 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
+ import "./src-D5Y3N-S4.js";
7
+ import { t as isSQLAdmin } from "./backend-DTAOsLQc.js";
8
+ import { r as logger } from "./logger-D-S-hO5e.js";
9
+ import { n as revokeInternalTableSql, t as REBASE_USER_ROLE } from "./internal-tables-L2t39RdT.js";
10
+ import { n as createDdlBootstrapper, r as hasInCauseChain } from "./ddl-bootstrap-BaqMSa4Y.js";
11
+ //#region src/cron/cron-store.ts
12
+ var cron_store_exports = /* @__PURE__ */ __exportAll({ createCronStore: () => createCronStore });
13
+ var TABLE = "rebase.cron_logs";
14
+ var CLAIMS_TABLE = "rebase.cron_claims";
15
+ var STATE_TABLE = "rebase.cron_job_state";
16
+ /** `$1, $2, …` for `count` values, starting at `$first`. */
17
+ function placeholders(count, first = 1) {
18
+ return Array.from({ length: count }, (_, i) => `$${first + i}`).join(", ");
19
+ }
20
+ /**
21
+ * Claims older than this are garbage-collected — all but each job's most
22
+ * recent one, which is kept however old it is; see `sweepClaims`.
23
+ */
24
+ var CLAIM_RETENTION_DAYS = 7;
25
+ /** Claims one retention statement deletes; a sweep repeats it until a batch comes back short. */
26
+ var CLAIM_SWEEP_BATCH = 5e3;
27
+ /**
28
+ * How far ahead a claim may legitimately sit. Slots are claimed as they fire,
29
+ * so anything beyond this is a clock-skewed peer at best and a stranded claim
30
+ * at worst; see the sweep in `ensureTable`.
31
+ */
32
+ var FUTURE_CLAIM_SKEW_MINUTES = 2;
33
+ /**
34
+ * How long a run stays in `cron_logs`, which is what the history panel and
35
+ * `GET /api/admin/cron/:id/logs` show. The run and failure counts do not
36
+ * shrink with it: they are kept on `cron_job_state` for all time.
37
+ *
38
+ * Nothing used to delete from `cron_logs`: twelve ordinary jobs wrote 1.5M rows
39
+ * and 617 MB a year, and the counts were `count(*)` over all of it on every
40
+ * poll of the Studio panel.
41
+ */
42
+ var LOG_RETENTION_DAYS = 30;
43
+ /** History rows one retention or counting statement handles; a sweep repeats it until a batch comes back short. */
44
+ var LOG_SWEEP_BATCH = 5e3;
45
+ /**
46
+ * Whether the run being folded in is at least as recent as the job's last
47
+ * one. The last-run fields only move forward: a long run is logged when it
48
+ * ends, after the skip that started while it ran, and a counting batch can
49
+ * hold any run of the job's history.
50
+ */
51
+ var IS_NEWER_RUN = "(s.last_run_at IS NULL OR EXCLUDED.last_run_at >= s.last_run_at)";
52
+ /**
53
+ * Add a set of runs to their jobs' counters on `cron_job_state`, as one
54
+ * `INSERT … ON CONFLICT`. `runs` names a relation of the runs —
55
+ * `job_id, started_at, duration_ms, success, error` — and becomes one row per
56
+ * job: how many, how many failed, and the most recent of them.
57
+ *
58
+ * Ordered by job, so two statements folding the same jobs lock their rows in
59
+ * the same order and cannot deadlock.
60
+ */
61
+ function foldIntoCounters(runs) {
62
+ return `
63
+ INSERT INTO ${STATE_TABLE} AS s
64
+ (job_id, total_runs, total_failures, last_run_at, last_duration_ms, last_success, last_error)
65
+ SELECT tally.job_id, tally.runs, tally.failures,
66
+ latest.started_at, latest.duration_ms, latest.success, latest.error
67
+ FROM (
68
+ SELECT job_id, count(*) AS runs, count(*) FILTER (WHERE NOT success) AS failures
69
+ FROM ${runs} GROUP BY job_id
70
+ ) AS tally
71
+ JOIN (
72
+ SELECT DISTINCT ON (job_id) job_id, started_at, duration_ms, success, error
73
+ FROM ${runs} ORDER BY job_id, started_at DESC
74
+ ) AS latest ON latest.job_id = tally.job_id
75
+ ORDER BY tally.job_id
76
+ ON CONFLICT (job_id) DO UPDATE SET
77
+ total_runs = s.total_runs + EXCLUDED.total_runs,
78
+ total_failures = s.total_failures + EXCLUDED.total_failures,
79
+ last_run_at = CASE WHEN ${IS_NEWER_RUN} THEN EXCLUDED.last_run_at ELSE s.last_run_at END,
80
+ last_duration_ms = CASE WHEN ${IS_NEWER_RUN} THEN EXCLUDED.last_duration_ms ELSE s.last_duration_ms END,
81
+ last_success = CASE WHEN ${IS_NEWER_RUN} THEN EXCLUDED.last_success ELSE s.last_success END,
82
+ last_error = CASE WHEN ${IS_NEWER_RUN} THEN EXCLUDED.last_error ELSE s.last_error END`;
83
+ }
84
+ /**
85
+ * SQLSTATEs that mean the counters are not there to write to — a column or
86
+ * table a failed boot did not create, or a privilege it did not get. A log
87
+ * insert that meets one is written without its count; the next sweep counts
88
+ * it. Anything else is not retried, because a statement that failed after it
89
+ * committed would then be logged — and counted — twice.
90
+ */
91
+ var COUNTERS_MISSING_SQLSTATES = /* @__PURE__ */ new Set([
92
+ "42703",
93
+ "42P01",
94
+ "42501"
95
+ ]);
96
+ /**
97
+ * Detect a unique-constraint violation anywhere in an error's cause chain.
98
+ * Match the SQLSTATE code, never message text. Also covers SQLite
99
+ * ("UNIQUE constraint failed") and MySQL (ER_DUP_ENTRY 1062) for future SQL
100
+ * drivers.
101
+ *
102
+ * Distinct from `isConcurrentDdlRace` in `boot/ddl-bootstrap.ts`, which shares
103
+ * the 23505 code but asks a different question — that one is about a losing
104
+ * `CREATE`, this one is about a losing claim, and only the latter means
105
+ * "another instance already has this slot".
106
+ */
107
+ function isUniqueViolation(err) {
108
+ return hasInCauseChain(err, (e) => e.code === "23505" || e.errno === 1062 || typeof e.message === "string" && e.message.includes("UNIQUE constraint failed"));
109
+ }
110
+ /**
111
+ * The setup each database is going through right now, in this process. The
112
+ * scheduler, the expired-token sweep and the history retention sweep each make
113
+ * a store on the same driver, and three setups at once ran the same REVOKEs
114
+ * concurrently — which Postgres can answer with "tuple concurrently updated",
115
+ * failing a security control at boot. Keyed on the driver, so a second
116
+ * database gets a setup of its own.
117
+ *
118
+ * Only the attempt in flight is shared. A later call runs the setup again —
119
+ * every statement in it is idempotent — because the tables can be gone by
120
+ * then: a database reset under a running server, or a suite that drops the
121
+ * schema between tests. A process that remembered "set up" would never bring
122
+ * them back.
123
+ */
124
+ var setUpsInFlight = /* @__PURE__ */ new WeakMap();
125
+ /** Databases this process has already said "ready" for, so it says so once. */
126
+ var announcedReady = /* @__PURE__ */ new WeakSet();
127
+ function ensureSharedInFlight(driver, setUp) {
128
+ const current = setUpsInFlight.get(driver);
129
+ if (current) return current;
130
+ const attempt = setUp().then(() => void 0).finally(() => {
131
+ if (setUpsInFlight.get(driver) === attempt) setUpsInFlight.delete(driver);
132
+ });
133
+ setUpsInFlight.set(driver, attempt);
134
+ return attempt;
135
+ }
136
+ function createCronStore(driver) {
137
+ const admin = driver.admin;
138
+ if (!isSQLAdmin(admin)) {
139
+ logger.warn("⚠️ [cron-store] DataDriver does not support SQL admin — cron logs will not be persisted.");
140
+ return;
141
+ }
142
+ const exec = (sqlText, options) => admin.executeSql(sqlText, options?.params ? { params: options.params } : void 0);
143
+ const ddl = createDdlBootstrapper(exec, "cron-store");
144
+ /** Whether `rebase.<table>` has `column`. False when it cannot tell, so the idempotent ALTER still runs. */
145
+ const hasColumn = async (table, column) => {
146
+ try {
147
+ return (await exec(`SELECT 1 FROM information_schema.columns
148
+ WHERE table_schema = 'rebase' AND table_name = $1 AND column_name = $2`, { params: [table, column] }) ?? []).length > 0;
149
+ } catch {
150
+ return false;
151
+ }
152
+ };
153
+ /**
154
+ * Whether a boot has nothing to do: the three tables, the three indexes and
155
+ * the run-counter columns are there, the dropped index is gone, and the
156
+ * end-user role holds no privilege on any of the tables. One catalogue
157
+ * read, where every boot used to run eleven statements — `CREATE … IF NOT
158
+ * EXISTS` takes its table's lock before it finds the object there, and a
159
+ * `REVOKE` rewrites the table's catalogue row even when nothing is granted.
160
+ * Anything missing, or a read that fails, and the boot runs them all.
161
+ */
162
+ const bootIsCurrent = async () => {
163
+ const objects = [
164
+ "cron_logs",
165
+ "cron_claims",
166
+ "cron_job_state",
167
+ "idx_cron_logs_job",
168
+ "idx_cron_logs_uncounted",
169
+ "idx_cron_logs_started"
170
+ ];
171
+ const columns = ["cron_logs.counted", ...[
172
+ "total_runs",
173
+ "total_failures",
174
+ "last_run_at",
175
+ "last_duration_ms",
176
+ "last_success",
177
+ "last_error"
178
+ ].map((c) => `cron_job_state.${c}`)];
179
+ const quoted = (names) => names.map((name) => `'${name}'`).join(", ");
180
+ try {
181
+ const row = (await exec(`
182
+ WITH objects AS (
183
+ SELECT c.oid, c.relname, c.relkind, c.relrowsecurity, c.relacl FROM pg_class c
184
+ WHERE c.relnamespace = to_regnamespace('rebase')
185
+ ), target AS (
186
+ SELECT oid FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'
187
+ )
188
+ SELECT
189
+ (SELECT count(*) FROM objects WHERE relname IN (${quoted(objects)}))::int AS present,
190
+ (SELECT count(*) FROM objects WHERE relname = 'idx_cron_logs_job_failures')::int AS stale,
191
+ (SELECT count(*) FROM information_schema.columns
192
+ WHERE table_schema = 'rebase' AND table_name || '.' || column_name IN (${quoted(columns)}))::int AS columns,
193
+ (SELECT count(*) FROM objects o
194
+ WHERE o.relkind = 'r' AND o.relname IN ('cron_logs', 'cron_claims', 'cron_job_state')
195
+ AND NOT o.relrowsecurity AND EXISTS (SELECT 1 FROM target)
196
+ AND (o.relacl IS NULL OR EXISTS (
197
+ SELECT 1 FROM aclexplode(o.relacl) a WHERE a.grantee = (SELECT oid FROM target)
198
+ )))::int AS granted
199
+ `))?.[0];
200
+ return Number(row?.present) === objects.length && Number(row?.stale) === 0 && Number(row?.columns) === columns.length && Number(row?.granted) === 0;
201
+ } catch {
202
+ return false;
203
+ }
204
+ };
205
+ /**
206
+ * Drop claims for slots that have not happened yet. A slot is claimed at
207
+ * the moment it fires, so a future one can only come from a timer that
208
+ * woke early — and because claims are permanent, that claim would silently
209
+ * skip the real run when it finally came due. The margin keeps a legitimate
210
+ * claim made moments early by a clock-skewed peer.
211
+ */
212
+ const releaseFutureClaims = () => ddl.step("Future-slot claim sweep", async () => {
213
+ const stranded = await exec(`DELETE FROM ${CLAIMS_TABLE}
214
+ WHERE slot > now() + make_interval(mins => $1)
215
+ RETURNING job_id, slot`, { params: [FUTURE_CLAIM_SKEW_MINUTES] });
216
+ for (const row of stranded ?? []) logger.warn(`[cron-store] Released a claim on the future slot ${new Date(row.slot).toISOString()} for "${row.job_id}" — it was claimed by a timer that fired early, and would otherwise have skipped that run`);
217
+ });
218
+ /** Run one sweep statement until a batch comes back short. Resolves with the total of `column`. */
219
+ const inBatches = async (sqlText, params, column) => {
220
+ let total = 0;
221
+ for (;;) {
222
+ const rows = await exec(sqlText, { params });
223
+ const done = Number((rows?.[0])?.[column] ?? 0);
224
+ total += done;
225
+ if (done < LOG_SWEEP_BATCH) return total;
226
+ }
227
+ };
228
+ /**
229
+ * The boot setup. Resolves with whether every table came up and was locked
230
+ * away; anything short of that has been reported, and is worth another try.
231
+ */
232
+ const announceReady = () => {
233
+ if (announcedReady.has(driver)) return;
234
+ announcedReady.add(driver);
235
+ logger.info("✅ Cron logs table ready");
236
+ };
237
+ const setUp = async () => {
238
+ if (await bootIsCurrent()) {
239
+ await releaseFutureClaims();
240
+ announceReady();
241
+ return true;
242
+ }
243
+ await ddl.ensureObject("Creating schema rebase", "CREATE SCHEMA IF NOT EXISTS rebase");
244
+ await ddl.ensureObject(`Creating ${TABLE}`, `
245
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
246
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
247
+ job_id TEXT NOT NULL,
248
+ started_at TIMESTAMPTZ NOT NULL,
249
+ finished_at TIMESTAMPTZ NOT NULL,
250
+ duration_ms INTEGER NOT NULL,
251
+ success BOOLEAN NOT NULL DEFAULT true,
252
+ error TEXT,
253
+ result JSONB,
254
+ logs JSONB,
255
+ manual BOOLEAN NOT NULL DEFAULT false
256
+ )
257
+ `);
258
+ await ddl.ensureObject("Creating idx_cron_logs_job", `
259
+ CREATE INDEX IF NOT EXISTS idx_cron_logs_job
260
+ ON ${TABLE}(job_id, started_at DESC)
261
+ `);
262
+ if (!await hasColumn("cron_logs", "counted")) await ddl.ensureObject(`Adding ${TABLE}.counted`, `
263
+ ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS counted BOOLEAN
264
+ `);
265
+ await ddl.ensureObject("Creating idx_cron_logs_uncounted", `
266
+ CREATE INDEX IF NOT EXISTS idx_cron_logs_uncounted
267
+ ON ${TABLE}(job_id, started_at DESC) WHERE counted IS NULL
268
+ `);
269
+ await ddl.ensureObject("Creating idx_cron_logs_started", `
270
+ CREATE INDEX IF NOT EXISTS idx_cron_logs_started
271
+ ON ${TABLE}(started_at)
272
+ `);
273
+ await ddl.ensureObject("Dropping idx_cron_logs_job_failures", "DROP INDEX IF EXISTS rebase.idx_cron_logs_job_failures");
274
+ await ddl.ensureObject(`Creating ${CLAIMS_TABLE}`, `
275
+ CREATE TABLE IF NOT EXISTS ${CLAIMS_TABLE} (
276
+ job_id TEXT NOT NULL,
277
+ slot TIMESTAMPTZ NOT NULL,
278
+ claimed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
279
+ PRIMARY KEY (job_id, slot)
280
+ )
281
+ `);
282
+ await ddl.ensureObject(`Creating ${STATE_TABLE}`, `
283
+ CREATE TABLE IF NOT EXISTS ${STATE_TABLE} (
284
+ job_id TEXT PRIMARY KEY,
285
+ enabled BOOLEAN,
286
+ updated_at TIMESTAMPTZ,
287
+ updated_by TEXT,
288
+ running_until TIMESTAMPTZ,
289
+ running_by TEXT
290
+ )
291
+ `);
292
+ if (!await hasColumn("cron_job_state", "total_runs")) await ddl.ensureObject(`Adding the run counters to ${STATE_TABLE}`, `
293
+ ALTER TABLE ${STATE_TABLE}
294
+ ADD COLUMN IF NOT EXISTS total_runs BIGINT NOT NULL DEFAULT 0,
295
+ ADD COLUMN IF NOT EXISTS total_failures BIGINT NOT NULL DEFAULT 0,
296
+ ADD COLUMN IF NOT EXISTS last_run_at TIMESTAMPTZ,
297
+ ADD COLUMN IF NOT EXISTS last_duration_ms INTEGER,
298
+ ADD COLUMN IF NOT EXISTS last_success BOOLEAN,
299
+ ADD COLUMN IF NOT EXISTS last_error TEXT
300
+ `);
301
+ const [logsReady, claimsReady, stateReady] = await Promise.all([
302
+ ddl.isReadable(TABLE),
303
+ ddl.isReadable(CLAIMS_TABLE),
304
+ ddl.isReadable(STATE_TABLE)
305
+ ]);
306
+ if (claimsReady) await releaseFutureClaims();
307
+ if (logsReady) await ddl.step("Revoking end-user access to cron_logs", () => exec(revokeInternalTableSql("rebase", "cron_logs")));
308
+ if (claimsReady) await ddl.step("Revoking end-user access to cron_claims", () => exec(revokeInternalTableSql("rebase", "cron_claims")));
309
+ if (stateReady) await ddl.step("Revoking end-user access to cron_job_state", () => exec(revokeInternalTableSql("rebase", "cron_job_state")));
310
+ if (logsReady && claimsReady && stateReady) {
311
+ announceReady();
312
+ return true;
313
+ }
314
+ if (!claimsReady) logger.error(`❌ [cron-store] ${CLAIMS_TABLE} is unavailable — scheduled runs cannot be coordinated. With more than one app instance, every instance will now run every job on every tick.`);
315
+ if (!logsReady) logger.warn(`⚠️ [cron-store] ${TABLE} is unavailable — cron run history will not be persisted.`);
316
+ if (!stateReady) logger.warn(`⚠️ [cron-store] ${STATE_TABLE} is unavailable — pausing a job reaches only the process that served the request, and jobs run as their code declares on every other one. A manual trigger is not kept off a run another process is making.`);
317
+ return false;
318
+ };
319
+ return {
320
+ ensureTable: () => ensureSharedInFlight(driver, setUp),
321
+ async sweepClaims() {
322
+ let total = 0;
323
+ for (;;) {
324
+ const rows = await exec(`WITH swept AS (
325
+ DELETE FROM ${CLAIMS_TABLE}
326
+ WHERE (job_id, slot) IN (
327
+ SELECT c.job_id, c.slot FROM ${CLAIMS_TABLE} AS c
328
+ WHERE c.claimed_at < now() - make_interval(days => $1)
329
+ AND c.slot < (
330
+ SELECT max(latest.slot) FROM ${CLAIMS_TABLE} AS latest
331
+ WHERE latest.job_id = c.job_id
332
+ AND latest.slot <= now() + make_interval(mins => $2)
333
+ )
334
+ LIMIT $3
335
+ FOR UPDATE SKIP LOCKED
336
+ )
337
+ RETURNING 1
338
+ )
339
+ SELECT count(*)::int AS deleted FROM swept`, { params: [
340
+ CLAIM_RETENTION_DAYS,
341
+ FUTURE_CLAIM_SKEW_MINUTES,
342
+ CLAIM_SWEEP_BATCH
343
+ ] });
344
+ const deleted = Number((rows?.[0])?.deleted ?? 0);
345
+ total += deleted;
346
+ if (deleted < CLAIM_SWEEP_BATCH) return total;
347
+ }
348
+ },
349
+ async sweepLogs() {
350
+ return {
351
+ deleted: await inBatches(`WITH gone AS (
352
+ DELETE FROM ${TABLE}
353
+ WHERE id IN (
354
+ SELECT id FROM ${TABLE}
355
+ WHERE started_at < now() - make_interval(days => $1)
356
+ ORDER BY started_at
357
+ LIMIT $2
358
+ FOR UPDATE SKIP LOCKED
359
+ )
360
+ RETURNING job_id, counted, started_at, duration_ms, success, error
361
+ ), uncounted AS (
362
+ SELECT job_id, started_at, duration_ms, success, error FROM gone WHERE counted IS NULL
363
+ ), folded AS (
364
+ ${foldIntoCounters("uncounted")}
365
+ RETURNING 1
366
+ )
367
+ SELECT count(*)::int AS deleted FROM gone`, [LOG_RETENTION_DAYS, LOG_SWEEP_BATCH], "deleted"),
368
+ counted: await inBatches(`WITH batch AS (
369
+ SELECT id FROM ${TABLE}
370
+ WHERE counted IS NULL
371
+ ORDER BY job_id, started_at DESC
372
+ LIMIT $1
373
+ FOR UPDATE SKIP LOCKED
374
+ ), marked AS (
375
+ UPDATE ${TABLE} AS l SET counted = true
376
+ FROM batch WHERE l.id = batch.id
377
+ RETURNING l.job_id, l.started_at, l.duration_ms, l.success, l.error
378
+ ), folded AS (
379
+ ${foldIntoCounters("marked")}
380
+ RETURNING 1
381
+ )
382
+ SELECT count(*)::int AS counted FROM marked`, [LOG_SWEEP_BATCH], "counted")
383
+ };
384
+ },
385
+ async insertLog(entry) {
386
+ const resultJson = entry.result !== void 0 ? JSON.stringify(entry.result) : null;
387
+ const logsJson = entry.logs.length > 0 ? JSON.stringify(entry.logs) : null;
388
+ const params = [
389
+ entry.jobId,
390
+ entry.startedAt,
391
+ entry.finishedAt,
392
+ entry.durationMs,
393
+ entry.success,
394
+ entry.error || null,
395
+ resultJson,
396
+ logsJson,
397
+ entry.manual
398
+ ];
399
+ const columns = "job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual";
400
+ const values = "$1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, $9";
401
+ try {
402
+ try {
403
+ await exec(`WITH logged AS (
404
+ INSERT INTO ${TABLE} (${columns}, counted)
405
+ VALUES (${values}, true)
406
+ RETURNING job_id, started_at, duration_ms, success, error
407
+ )
408
+ ${foldIntoCounters("logged")}`, { params });
409
+ } catch (err) {
410
+ if (!hasInCauseChain(err, (e) => typeof e.code === "string" && COUNTERS_MISSING_SQLSTATES.has(e.code))) throw err;
411
+ await exec(`INSERT INTO ${TABLE} (${columns}) VALUES (${values})`, { params });
412
+ }
413
+ } catch (err) {
414
+ logger.error(`[cron-store] Failed to persist log for "${entry.jobId}"`, { error: err });
415
+ }
416
+ },
417
+ async fetchLogs(jobId, limit = 50) {
418
+ return (await exec(`SELECT job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual
419
+ FROM ${TABLE}
420
+ WHERE job_id = $1
421
+ ORDER BY started_at DESC
422
+ LIMIT $2`, { params: [jobId, limit] })).map(rowToLogEntry);
423
+ },
424
+ async fetchJobStats() {
425
+ const stats = /* @__PURE__ */ new Map();
426
+ try {
427
+ const rows = await exec(`
428
+ SELECT job_id, total_runs, total_failures, last_run_at
429
+ FROM ${STATE_TABLE}
430
+ WHERE total_runs > 0
431
+ `);
432
+ for (const row of rows ?? []) stats.set(String(row.job_id), {
433
+ totalRuns: Number(row.total_runs),
434
+ totalFailures: Number(row.total_failures),
435
+ lastRunAt: toIsoString(row.last_run_at)
436
+ });
437
+ } catch (err) {
438
+ logger.error("[cron-store] Failed to fetch job stats", { error: err });
439
+ }
440
+ return stats;
441
+ },
442
+ async tryClaimRun(jobId, slot) {
443
+ try {
444
+ return (await exec(`INSERT INTO ${CLAIMS_TABLE} (job_id, slot)
445
+ VALUES ($1, $2)
446
+ ON CONFLICT (job_id, slot) DO NOTHING
447
+ RETURNING job_id`, { params: [jobId, slot] })).length > 0;
448
+ } catch (err) {
449
+ if (isUniqueViolation(err)) return false;
450
+ throw err;
451
+ }
452
+ },
453
+ async saveEnabledOverride(jobId, enabled, updatedBy) {
454
+ await exec(`INSERT INTO ${STATE_TABLE} (job_id, enabled, updated_at, updated_by)
455
+ VALUES ($1, $2::boolean, now(), $3)
456
+ ON CONFLICT (job_id) DO UPDATE
457
+ SET enabled = EXCLUDED.enabled, updated_at = now(), updated_by = EXCLUDED.updated_by`, { params: [
458
+ jobId,
459
+ enabled,
460
+ updatedBy ?? null
461
+ ] });
462
+ },
463
+ async fetchJobStates(jobIds) {
464
+ const states = /* @__PURE__ */ new Map();
465
+ if (jobIds.length === 0) return states;
466
+ const rows = await exec(`SELECT job_id, enabled,
467
+ CASE WHEN running_until > now() THEN running_by END AS running_by
468
+ FROM ${STATE_TABLE}
469
+ WHERE job_id IN (${placeholders(jobIds.length)})`, { params: [...jobIds] });
470
+ for (const row of rows ?? []) states.set(String(row.job_id), {
471
+ enabled: typeof row.enabled === "boolean" ? row.enabled : null,
472
+ runningBy: typeof row.running_by === "string" ? row.running_by : void 0
473
+ });
474
+ return states;
475
+ },
476
+ async tryAcquireRunLease(jobId, holder, ttlSeconds) {
477
+ const row = (await exec(`WITH attempt AS (
478
+ INSERT INTO ${STATE_TABLE} AS s (job_id, running_until, running_by)
479
+ VALUES ($1, now() + make_interval(secs => $2::double precision), $3)
480
+ ON CONFLICT (job_id) DO UPDATE
481
+ SET running_until = EXCLUDED.running_until, running_by = EXCLUDED.running_by
482
+ WHERE s.running_until IS NULL OR s.running_until <= now()
483
+ RETURNING s.job_id
484
+ )
485
+ SELECT EXISTS (SELECT 1 FROM attempt) AS acquired,
486
+ (SELECT running_by FROM ${STATE_TABLE}
487
+ WHERE job_id = $1 AND running_until > now()) AS holder`, { params: [
488
+ jobId,
489
+ ttlSeconds,
490
+ holder
491
+ ] }))?.[0];
492
+ if (!row) throw new Error(`The run lease for "${jobId}" got no answer from the database`);
493
+ if (row.acquired === true) return { acquired: true };
494
+ return {
495
+ acquired: false,
496
+ holder: typeof row.holder === "string" ? row.holder : void 0
497
+ };
498
+ },
499
+ async releaseRunLease(jobId, holder) {
500
+ await exec(`UPDATE ${STATE_TABLE}
501
+ SET running_until = NULL, running_by = NULL
502
+ WHERE job_id = $1 AND running_by = $2`, { params: [jobId, holder] });
503
+ },
504
+ async fetchRunSummaries(jobIds) {
505
+ const summaries = /* @__PURE__ */ new Map();
506
+ if (jobIds.length === 0) return summaries;
507
+ const rows = await exec(`SELECT j.job_id,
508
+ COALESCE(s.total_runs, 0) + COALESCE(pending.runs, 0) AS total_runs,
509
+ COALESCE(s.total_failures, 0) + COALESCE(pending.failures, 0) AS total_failures,
510
+ CASE WHEN newer.later THEN pending_last.started_at ELSE s.last_run_at END AS started_at,
511
+ CASE WHEN newer.later THEN pending_last.duration_ms ELSE s.last_duration_ms END AS duration_ms,
512
+ CASE WHEN newer.later THEN pending_last.success ELSE s.last_success END AS success,
513
+ CASE WHEN newer.later THEN pending_last.error ELSE s.last_error END AS error
514
+ FROM (VALUES ${jobIds.map((_, i) => `($${i + 1}::text)`).join(", ")}) AS j(job_id)
515
+ LEFT JOIN ${STATE_TABLE} AS s ON s.job_id = j.job_id
516
+ LEFT JOIN LATERAL (
517
+ SELECT count(*) AS runs, count(*) FILTER (WHERE NOT l.success) AS failures
518
+ FROM ${TABLE} l
519
+ WHERE l.job_id = j.job_id AND l.counted IS NULL
520
+ ) AS pending ON true
521
+ LEFT JOIN LATERAL (
522
+ SELECT l.started_at, l.duration_ms, l.success, l.error
523
+ FROM ${TABLE} l
524
+ WHERE l.job_id = j.job_id AND l.counted IS NULL
525
+ ORDER BY l.started_at DESC
526
+ LIMIT 1
527
+ ) AS pending_last ON true
528
+ CROSS JOIN LATERAL (
529
+ SELECT pending_last.started_at IS NOT NULL
530
+ AND (s.last_run_at IS NULL OR pending_last.started_at >= s.last_run_at) AS later
531
+ ) AS newer`, { params: [...jobIds] });
532
+ for (const row of rows ?? []) {
533
+ const summary = {
534
+ totalRuns: Number(row.total_runs ?? 0),
535
+ totalFailures: Number(row.total_failures ?? 0)
536
+ };
537
+ const lastRunAt = toIsoString(row.started_at);
538
+ if (lastRunAt) {
539
+ summary.lastRunAt = lastRunAt;
540
+ summary.lastDurationMs = Number(row.duration_ms);
541
+ summary.lastSuccess = row.success === true;
542
+ if (typeof row.error === "string") summary.lastError = row.error;
543
+ }
544
+ summaries.set(String(row.job_id), summary);
545
+ }
546
+ return summaries;
547
+ }
548
+ };
549
+ }
550
+ /** A TIMESTAMPTZ as the driver returned it — a `Date` or a string — in ISO form. */
551
+ function toIsoString(value) {
552
+ if (value instanceof Date) return value.toISOString();
553
+ if (typeof value === "string" || typeof value === "number") return new Date(value).toISOString();
554
+ }
555
+ function rowToLogEntry(row) {
556
+ return {
557
+ jobId: row.job_id,
558
+ startedAt: new Date(row.started_at).toISOString(),
559
+ finishedAt: new Date(row.finished_at).toISOString(),
560
+ durationMs: row.duration_ms,
561
+ success: row.success,
562
+ error: row.error ?? void 0,
563
+ result: row.result ?? void 0,
564
+ logs: Array.isArray(row.logs) ? row.logs : row.logs ? (() => {
565
+ try {
566
+ return JSON.parse(row.logs);
567
+ } catch {
568
+ return [];
569
+ }
570
+ })() : [],
571
+ manual: row.manual
572
+ };
573
+ }
574
+ //#endregion
575
+ export { cron_store_exports as n, createCronStore as t };
576
+
577
+ //# sourceMappingURL=cron-store-CZ30ZBi-.js.map