orez-lite 0.9.7
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/LICENSE +21 -0
- package/README.md +12 -0
- package/dist/browser/generated/sqlite3-browser.wasm +0 -0
- package/dist/browser/generated/sync_wasm.js +922 -0
- package/dist/browser/generated/sync_wasm_bg.wasm +0 -0
- package/dist/browser/host.d.ts +10 -0
- package/dist/browser/host.d.ts.map +1 -0
- package/dist/browser/host.js +496 -0
- package/dist/browser/host.js.map +1 -0
- package/dist/browser/idb-snapshot.d.ts +11 -0
- package/dist/browser/idb-snapshot.d.ts.map +1 -0
- package/dist/browser/idb-snapshot.js +122 -0
- package/dist/browser/idb-snapshot.js.map +1 -0
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +5 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/message-port.d.ts +7 -0
- package/dist/browser/message-port.d.ts.map +1 -0
- package/dist/browser/message-port.js +188 -0
- package/dist/browser/message-port.js.map +1 -0
- package/dist/browser/sqlite-adapter.d.ts +53 -0
- package/dist/browser/sqlite-adapter.d.ts.map +1 -0
- package/dist/browser/sqlite-adapter.js +126 -0
- package/dist/browser/sqlite-adapter.js.map +1 -0
- package/dist/browser/types.d.ts +52 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/cf-deploy/bundle.d.ts +18 -0
- package/dist/cf-deploy/bundle.d.ts.map +1 -0
- package/dist/cf-deploy/bundle.js +273 -0
- package/dist/cf-deploy/bundle.js.map +1 -0
- package/dist/cf-deploy/config.d.ts +12 -0
- package/dist/cf-deploy/config.d.ts.map +1 -0
- package/dist/cf-deploy/config.js +11 -0
- package/dist/cf-deploy/config.js.map +1 -0
- package/dist/cf-deploy/index.d.ts +10 -0
- package/dist/cf-deploy/index.d.ts.map +1 -0
- package/dist/cf-deploy/index.js +12 -0
- package/dist/cf-deploy/index.js.map +1 -0
- package/dist/cf-deploy/leaves.d.ts +8 -0
- package/dist/cf-deploy/leaves.d.ts.map +1 -0
- package/dist/cf-deploy/leaves.js +66 -0
- package/dist/cf-deploy/leaves.js.map +1 -0
- package/dist/cf-deploy/migration.d.ts +27 -0
- package/dist/cf-deploy/migration.d.ts.map +1 -0
- package/dist/cf-deploy/migration.js +734 -0
- package/dist/cf-deploy/migration.js.map +1 -0
- package/dist/cf-deploy/nativeMigrations.d.ts +24 -0
- package/dist/cf-deploy/nativeMigrations.d.ts.map +1 -0
- package/dist/cf-deploy/nativeMigrations.js +185 -0
- package/dist/cf-deploy/nativeMigrations.js.map +1 -0
- package/dist/cf-deploy/prune.d.ts +14 -0
- package/dist/cf-deploy/prune.d.ts.map +1 -0
- package/dist/cf-deploy/prune.js +235 -0
- package/dist/cf-deploy/prune.js.map +1 -0
- package/dist/cf-deploy/readiness.d.ts +11 -0
- package/dist/cf-deploy/readiness.d.ts.map +1 -0
- package/dist/cf-deploy/readiness.js +56 -0
- package/dist/cf-deploy/readiness.js.map +1 -0
- package/dist/cf-deploy/sources.d.ts +3 -0
- package/dist/cf-deploy/sources.d.ts.map +1 -0
- package/dist/cf-deploy/sources.js +24 -0
- package/dist/cf-deploy/sources.js.map +1 -0
- package/dist/cf-deploy/wrangler.d.ts +9 -0
- package/dist/cf-deploy/wrangler.d.ts.map +1 -0
- package/dist/cf-deploy/wrangler.js +48 -0
- package/dist/cf-deploy/wrangler.js.map +1 -0
- package/dist/cf-do/application-sql.d.ts +49 -0
- package/dist/cf-do/application-sql.d.ts.map +1 -0
- package/dist/cf-do/application-sql.js +124 -0
- package/dist/cf-do/application-sql.js.map +1 -0
- package/dist/cf-do/cdc.d.ts +155 -0
- package/dist/cf-do/cdc.d.ts.map +1 -0
- package/dist/cf-do/cdc.js +723 -0
- package/dist/cf-do/cdc.js.map +1 -0
- package/dist/cf-do/cursor-pull.d.ts +27 -0
- package/dist/cf-do/cursor-pull.d.ts.map +1 -0
- package/dist/cf-do/cursor-pull.js +78 -0
- package/dist/cf-do/cursor-pull.js.map +1 -0
- package/dist/cf-do/lite-data-worker.d.ts +171 -0
- package/dist/cf-do/lite-data-worker.d.ts.map +1 -0
- package/dist/cf-do/lite-data-worker.js +685 -0
- package/dist/cf-do/lite-data-worker.js.map +1 -0
- package/dist/cf-do/namespace-backup.d.ts +85 -0
- package/dist/cf-do/namespace-backup.d.ts.map +1 -0
- package/dist/cf-do/namespace-backup.js +470 -0
- package/dist/cf-do/namespace-backup.js.map +1 -0
- package/dist/cf-do/row-undo.d.ts +36 -0
- package/dist/cf-do/row-undo.d.ts.map +1 -0
- package/dist/cf-do/row-undo.js +221 -0
- package/dist/cf-do/row-undo.js.map +1 -0
- package/dist/cf-do/tx-journal.d.ts +118 -0
- package/dist/cf-do/tx-journal.d.ts.map +1 -0
- package/dist/cf-do/tx-journal.js +717 -0
- package/dist/cf-do/tx-journal.js.map +1 -0
- package/dist/cf-do/watermark.d.ts +29 -0
- package/dist/cf-do/watermark.d.ts.map +1 -0
- package/dist/cf-do/watermark.js +105 -0
- package/dist/cf-do/watermark.js.map +1 -0
- package/dist/cf-do/worker.d.ts +282 -0
- package/dist/cf-do/worker.d.ts.map +1 -0
- package/dist/cf-do/worker.js +1999 -0
- package/dist/cf-do/worker.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/cloudflare-build.d.ts +9 -0
- package/dist/cloudflare-build.d.ts.map +1 -0
- package/dist/cloudflare-build.js +9 -0
- package/dist/cloudflare-build.js.map +1 -0
- package/dist/cloudflare-bun-wasm-loader.d.ts +2 -0
- package/dist/cloudflare-bun-wasm-loader.d.ts.map +1 -0
- package/dist/cloudflare-bun-wasm-loader.js +2 -0
- package/dist/cloudflare-bun-wasm-loader.js.map +1 -0
- package/dist/cloudflare-query-compiler.d.ts +2 -0
- package/dist/cloudflare-query-compiler.d.ts.map +1 -0
- package/dist/cloudflare-query-compiler.js +2 -0
- package/dist/cloudflare-query-compiler.js.map +1 -0
- package/dist/cloudflare-runtime.d.ts +15 -0
- package/dist/cloudflare-runtime.d.ts.map +1 -0
- package/dist/cloudflare-runtime.js +11 -0
- package/dist/cloudflare-runtime.js.map +1 -0
- package/dist/cloudflare-sync.d.ts +3 -0
- package/dist/cloudflare-sync.d.ts.map +1 -0
- package/dist/cloudflare-sync.js +3 -0
- package/dist/cloudflare-sync.js.map +1 -0
- package/dist/cloudflare-transaction-query.d.ts +2 -0
- package/dist/cloudflare-transaction-query.d.ts.map +1 -0
- package/dist/cloudflare-transaction-query.js +2 -0
- package/dist/cloudflare-transaction-query.js.map +1 -0
- package/dist/cloudflare-vite-wasm-loader.d.ts +2 -0
- package/dist/cloudflare-vite-wasm-loader.d.ts.map +1 -0
- package/dist/cloudflare-vite-wasm-loader.js +2 -0
- package/dist/cloudflare-vite-wasm-loader.js.map +1 -0
- package/dist/cloudflare.d.ts +3 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +3 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/do-sql-tracking.d.ts +114 -0
- package/dist/do-sql-tracking.d.ts.map +1 -0
- package/dist/do-sql-tracking.js +280 -0
- package/dist/do-sql-tracking.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/wake.d.ts +22 -0
- package/dist/wake.d.ts.map +1 -0
- package/dist/wake.js +114 -0
- package/dist/wake.js.map +1 -0
- package/dist/worker/cf-do-shim.d.ts +53 -0
- package/dist/worker/cf-do-shim.d.ts.map +1 -0
- package/dist/worker/cf-do-shim.js +66 -0
- package/dist/worker/cf-do-shim.js.map +1 -0
- package/dist/zero-http/encrypted-column-codec.d.ts +46 -0
- package/dist/zero-http/encrypted-column-codec.d.ts.map +1 -0
- package/dist/zero-http/encrypted-column-codec.js +676 -0
- package/dist/zero-http/encrypted-column-codec.js.map +1 -0
- package/dist/zero-http/encrypted-column-conformance.d.ts +7 -0
- package/dist/zero-http/encrypted-column-conformance.d.ts.map +1 -0
- package/dist/zero-http/encrypted-column-conformance.js +161 -0
- package/dist/zero-http/encrypted-column-conformance.js.map +1 -0
- package/dist/zero-http/fixture-schema.d.ts +127 -0
- package/dist/zero-http/fixture-schema.d.ts.map +1 -0
- package/dist/zero-http/fixture-schema.js +56 -0
- package/dist/zero-http/fixture-schema.js.map +1 -0
- package/dist/zero-http/mount.d.ts +71 -0
- package/dist/zero-http/mount.d.ts.map +1 -0
- package/dist/zero-http/mount.js +594 -0
- package/dist/zero-http/mount.js.map +1 -0
- package/dist/zero-http/payload-codec.d.ts +91 -0
- package/dist/zero-http/payload-codec.d.ts.map +1 -0
- package/dist/zero-http/payload-codec.js +10 -0
- package/dist/zero-http/payload-codec.js.map +1 -0
- package/dist/zero-http/server.d.ts +16 -0
- package/dist/zero-http/server.d.ts.map +1 -0
- package/dist/zero-http/server.js +270 -0
- package/dist/zero-http/server.js.map +1 -0
- package/dist/zero-http/test-harness.d.ts +32 -0
- package/dist/zero-http/test-harness.d.ts.map +1 -0
- package/dist/zero-http/test-harness.js +77 -0
- package/dist/zero-http/test-harness.js.map +1 -0
- package/dist/zero-http/transport.d.ts +71 -0
- package/dist/zero-http/transport.d.ts.map +1 -0
- package/dist/zero-http/transport.js +1106 -0
- package/dist/zero-http/transport.js.map +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { stripPublicPrefix } from '../do-sql-tracking.js';
|
|
3
|
+
import { createApplicationSqlClient } from './application-sql.js';
|
|
4
|
+
import { createNamespaceBackupManager, } from './namespace-backup.js';
|
|
5
|
+
import { ZeroDO as OrezZeroDO } from './worker.js';
|
|
6
|
+
const STANDARD_DATA_PATHS = [
|
|
7
|
+
'/_orez/backup/restore',
|
|
8
|
+
'/_orez/backup/export',
|
|
9
|
+
'/_orez/write-budget/trip',
|
|
10
|
+
'/_orez/write-budget/reopen',
|
|
11
|
+
'/_orez/write-budget',
|
|
12
|
+
'/_orez/schema-status',
|
|
13
|
+
'/_orez/schema/migrate',
|
|
14
|
+
'/snapshot',
|
|
15
|
+
'/changes',
|
|
16
|
+
];
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function errorMessage(error) {
|
|
21
|
+
return error instanceof Error ? error.message : String(error);
|
|
22
|
+
}
|
|
23
|
+
function validScopedNamespace(value, options) {
|
|
24
|
+
const scopes = options.scopes ?? ['proj', 'test'];
|
|
25
|
+
return scopes.some((scope) => {
|
|
26
|
+
if (!scope || value.slice(0, scope.length + 1) !== `${scope}-`)
|
|
27
|
+
return false;
|
|
28
|
+
const id = value.slice(scope.length + 1);
|
|
29
|
+
return id.length >= 1 && id.length <= 64 && /^[A-Za-z0-9_-]+$/.test(id);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Accept a user-facing namespace or an already-canonical DO instance name.
|
|
34
|
+
* Every factory surface uses this function, so headers, path mounts, backups,
|
|
35
|
+
* SQL clients, and migration RPCs cannot disagree about object identity.
|
|
36
|
+
*/
|
|
37
|
+
export function canonicalOrezNamespace(namespace, options = {}) {
|
|
38
|
+
const value = String(namespace ?? '');
|
|
39
|
+
if (value === '' ||
|
|
40
|
+
value === 'singleton' ||
|
|
41
|
+
(options.controlPlaneNamespaces ?? []).includes(value)) {
|
|
42
|
+
return 'singleton';
|
|
43
|
+
}
|
|
44
|
+
const raw = value.startsWith('ns:') ? value.slice(3) : value;
|
|
45
|
+
return validScopedNamespace(raw, options) ? `ns:${raw}` : null;
|
|
46
|
+
}
|
|
47
|
+
function pathMountedRoute(pathname, options) {
|
|
48
|
+
for (const route of STANDARD_DATA_PATHS) {
|
|
49
|
+
if (!pathname.endsWith(route) || pathname === route)
|
|
50
|
+
continue;
|
|
51
|
+
const prefix = pathname.slice(0, -route.length);
|
|
52
|
+
if (!prefix.startsWith('/') || prefix.indexOf('/', 1) !== -1)
|
|
53
|
+
continue;
|
|
54
|
+
let namespace;
|
|
55
|
+
try {
|
|
56
|
+
namespace = decodeURIComponent(prefix.slice(1));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const instance = canonicalOrezNamespace(namespace, options);
|
|
62
|
+
if (instance)
|
|
63
|
+
return { instance, pathname: route };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve both standard root mounts (`/changes?ns=proj-a`) and Rust binding
|
|
69
|
+
* mounts (`/proj-a/changes`) to one canonical namespace and one route.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveOrezDataRequest(request, options = {}) {
|
|
72
|
+
const url = new URL(request.url);
|
|
73
|
+
const mounted = pathMountedRoute(url.pathname, options);
|
|
74
|
+
if (mounted) {
|
|
75
|
+
url.pathname = mounted.pathname;
|
|
76
|
+
return { ...mounted, url };
|
|
77
|
+
}
|
|
78
|
+
const namespace = request.headers.get(options.nsHeader ?? 'x-orez-ns') ||
|
|
79
|
+
url.searchParams.get('ns') ||
|
|
80
|
+
'';
|
|
81
|
+
const instance = canonicalOrezNamespace(namespace, options);
|
|
82
|
+
return instance ? { instance, pathname: url.pathname, url } : null;
|
|
83
|
+
}
|
|
84
|
+
function schemaFeedTables(descriptor) {
|
|
85
|
+
const result = new Map();
|
|
86
|
+
const schemaTables = Object.entries(descriptor.schema.tables);
|
|
87
|
+
for (const published of descriptor.publicTables) {
|
|
88
|
+
const publicName = stripPublicPrefix(published.publicTable);
|
|
89
|
+
const matched = schemaTables.find(([logicalName, table]) => {
|
|
90
|
+
const physicalName = table.serverName ?? table.name;
|
|
91
|
+
return (physicalName === published.table ||
|
|
92
|
+
table.name === publicName ||
|
|
93
|
+
logicalName === publicName);
|
|
94
|
+
});
|
|
95
|
+
if (!matched) {
|
|
96
|
+
throw new TypeError(`public table ${JSON.stringify(published.publicTable)} (${JSON.stringify(published.table)}) is absent from the Zero schema`);
|
|
97
|
+
}
|
|
98
|
+
const [logicalName, table] = matched;
|
|
99
|
+
const columns = new Map();
|
|
100
|
+
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
101
|
+
columns.set(column.serverName ?? columnName, columnName);
|
|
102
|
+
columns.set(columnName, columnName);
|
|
103
|
+
}
|
|
104
|
+
const projection = { name: logicalName, columns };
|
|
105
|
+
for (const alias of [
|
|
106
|
+
published.table,
|
|
107
|
+
published.publicTable,
|
|
108
|
+
publicName,
|
|
109
|
+
table.name,
|
|
110
|
+
table.serverName,
|
|
111
|
+
logicalName,
|
|
112
|
+
]) {
|
|
113
|
+
if (alias)
|
|
114
|
+
result.set(stripPublicPrefix(alias), projection);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
function projectedRow(table, row) {
|
|
120
|
+
if (!isRecord(row))
|
|
121
|
+
return row;
|
|
122
|
+
const projected = {};
|
|
123
|
+
for (const [source, target] of table.columns) {
|
|
124
|
+
if (source in row && !(target in projected))
|
|
125
|
+
projected[target] = row[source];
|
|
126
|
+
}
|
|
127
|
+
return projected;
|
|
128
|
+
}
|
|
129
|
+
function isInternalCursorTable(tableName) {
|
|
130
|
+
const name = stripPublicPrefix(tableName);
|
|
131
|
+
return (name === '_zsync_clients' || /^[A-Za-z0-9_]+_0\.(?:clients|mutations)$/.test(name));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Convert the authoritative SQLite response into the actual public Zero
|
|
135
|
+
* schema. Unknown/unpublished tables and columns are removed. Internal
|
|
136
|
+
* mutation-cursor sources are projected to `syncCursor` before table lookup,
|
|
137
|
+
* so applications never add them to their schema or a column allow-list.
|
|
138
|
+
*/
|
|
139
|
+
export function projectOrezFeedBody(descriptor, value, snapshotTable) {
|
|
140
|
+
return projectFeedBody(schemaFeedTables(descriptor), value, snapshotTable);
|
|
141
|
+
}
|
|
142
|
+
function projectFeedBody(feedTables, value, snapshotTable) {
|
|
143
|
+
if (!isRecord(value))
|
|
144
|
+
return value;
|
|
145
|
+
if (feedTables.size === 0) {
|
|
146
|
+
throw new Error('Orez data feeds require at least one published Zero table');
|
|
147
|
+
}
|
|
148
|
+
const projected = { ...value };
|
|
149
|
+
if (snapshotTable && Array.isArray(value.rows)) {
|
|
150
|
+
if (snapshotTable === 'syncCursor') {
|
|
151
|
+
projected.rows = [{ id: 'zero-http', watermark: Number(value.watermark ?? 0) }];
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const table = feedTables.get(stripPublicPrefix(snapshotTable));
|
|
155
|
+
if (!table) {
|
|
156
|
+
throw new Error(`snapshot table ${JSON.stringify(snapshotTable)} is not published`);
|
|
157
|
+
}
|
|
158
|
+
projected.rows = value.rows.map((row) => projectedRow(table, row));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (isRecord(value.tables)) {
|
|
162
|
+
const tables = {};
|
|
163
|
+
for (const [rawName, rows] of Object.entries(value.tables)) {
|
|
164
|
+
const table = feedTables.get(stripPublicPrefix(rawName));
|
|
165
|
+
if (!table)
|
|
166
|
+
continue;
|
|
167
|
+
tables[table.name] = Array.isArray(rows)
|
|
168
|
+
? rows.map((row) => projectedRow(table, row))
|
|
169
|
+
: rows;
|
|
170
|
+
}
|
|
171
|
+
tables.syncCursor = [{ id: 'zero-http', watermark: Number(value.watermark ?? 0) }];
|
|
172
|
+
projected.tables = tables;
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(value.changes)) {
|
|
175
|
+
const changes = [];
|
|
176
|
+
for (const rawChange of value.changes) {
|
|
177
|
+
if (!isRecord(rawChange))
|
|
178
|
+
continue;
|
|
179
|
+
const rawName = stripPublicPrefix(String(rawChange.tableName ?? ''));
|
|
180
|
+
if (isInternalCursorTable(rawName)) {
|
|
181
|
+
changes.push({
|
|
182
|
+
watermark: rawChange.watermark,
|
|
183
|
+
tableName: 'syncCursor',
|
|
184
|
+
op: 'INSERT',
|
|
185
|
+
rowData: { id: 'zero-http', watermark: rawChange.watermark },
|
|
186
|
+
oldData: null,
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const table = feedTables.get(rawName);
|
|
191
|
+
if (!table)
|
|
192
|
+
continue;
|
|
193
|
+
changes.push({
|
|
194
|
+
...rawChange,
|
|
195
|
+
tableName: table.name,
|
|
196
|
+
rowData: projectedRow(table, rawChange.rowData),
|
|
197
|
+
oldData: projectedRow(table, rawChange.oldData),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
projected.changes = changes;
|
|
201
|
+
}
|
|
202
|
+
return projected;
|
|
203
|
+
}
|
|
204
|
+
function requireAppName(name) {
|
|
205
|
+
if (!/^[a-z][a-z0-9]*$/.test(name)) {
|
|
206
|
+
throw new TypeError(`Orez data worker name must be a lowercase identifier, got ${JSON.stringify(name)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Build the complete application-data Worker and the concrete SQLite Durable
|
|
211
|
+
* Object class Cloudflare requires. Consumers export the returned class; they
|
|
212
|
+
* never subclass Orez internals.
|
|
213
|
+
*/
|
|
214
|
+
export function createOrezDataWorker(options) {
|
|
215
|
+
requireAppName(options.name);
|
|
216
|
+
const tablePrefix = options.tablePrefix ?? '_orez';
|
|
217
|
+
if (!/^_[a-z][a-z0-9_]*$/.test(tablePrefix)) {
|
|
218
|
+
throw new TypeError(`Orez tablePrefix must be an underscore-prefixed lowercase identifier, got ${JSON.stringify(tablePrefix)}`);
|
|
219
|
+
}
|
|
220
|
+
// Validate once and retain the immutable projection for the worker lifetime.
|
|
221
|
+
const feedTables = schemaFeedTables(options.schema);
|
|
222
|
+
const namespaceOptions = {
|
|
223
|
+
...options.namespace,
|
|
224
|
+
nsHeader: options.namespace?.nsHeader ?? 'x-orez-ns',
|
|
225
|
+
};
|
|
226
|
+
const suspendAfterFailures = options.migrations?.suspendAfterFailures ?? 20;
|
|
227
|
+
if (!Number.isSafeInteger(suspendAfterFailures) || suspendAfterFailures < 1) {
|
|
228
|
+
throw new TypeError('migrations.suspendAfterFailures must be a positive safe integer');
|
|
229
|
+
}
|
|
230
|
+
for (const [name, value] of Object.entries(options.writeBudget ?? {})) {
|
|
231
|
+
if (!Number.isSafeInteger(value) || Number(value) < 1) {
|
|
232
|
+
throw new TypeError(`writeBudget.${name} must be a positive safe integer`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const requestSignals = new AsyncLocalStorage();
|
|
236
|
+
const readyTable = `${tablePrefix}_application_schema`;
|
|
237
|
+
const attemptTable = `${tablePrefix}_application_schema_attempt`;
|
|
238
|
+
const backupMarkerTable = `${tablePrefix}_backup_meta`;
|
|
239
|
+
const restoreTable = `${tablePrefix}_restore`;
|
|
240
|
+
const applicationClientGlobal = `__${options.name}_cf_application_sql_client`;
|
|
241
|
+
const bucketGlobal = `__${options.name}_cf_r2_bucket`;
|
|
242
|
+
const canonical = (namespace) => {
|
|
243
|
+
const instance = canonicalOrezNamespace(namespace, namespaceOptions);
|
|
244
|
+
if (!instance)
|
|
245
|
+
throw new TypeError('invalid application SQLite namespace');
|
|
246
|
+
return instance;
|
|
247
|
+
};
|
|
248
|
+
const applicationSqlClient = (env, namespace = 'singleton', signal = requestSignals.getStore()) => createApplicationSqlClient(env.ZERO_SQL_DO, canonical(namespace), { signal });
|
|
249
|
+
const installRuntimeGlobals = (env) => {
|
|
250
|
+
const globals = globalThis;
|
|
251
|
+
globals[applicationClientGlobal] = (namespace = 'singleton') => applicationSqlClient(env, String(namespace));
|
|
252
|
+
if (options.backup)
|
|
253
|
+
globals[bucketGlobal] = options.backup.bucket(env);
|
|
254
|
+
};
|
|
255
|
+
class ZeroSqlDO extends OrezZeroDO {
|
|
256
|
+
orezStorage;
|
|
257
|
+
orezWorkerVersion;
|
|
258
|
+
orezSchemaRunVersion = null;
|
|
259
|
+
orezSchemaRun = null;
|
|
260
|
+
orezReadyVersion = null;
|
|
261
|
+
constructor(ctx, env) {
|
|
262
|
+
// The base DO's private Env includes its historical binding spelling;
|
|
263
|
+
// its constructor only consumes the write-budget environment fields.
|
|
264
|
+
super(ctx, (options.writeBudget
|
|
265
|
+
? {
|
|
266
|
+
...env,
|
|
267
|
+
...(options.writeBudget.rows
|
|
268
|
+
? { OREZ_DO_WRITE_BUDGET_ROWS: String(options.writeBudget.rows) }
|
|
269
|
+
: null),
|
|
270
|
+
...(options.writeBudget.windowMs
|
|
271
|
+
? {
|
|
272
|
+
OREZ_DO_WRITE_BUDGET_WINDOW_MS: String(options.writeBudget.windowMs),
|
|
273
|
+
}
|
|
274
|
+
: null),
|
|
275
|
+
}
|
|
276
|
+
: env));
|
|
277
|
+
this.orezStorage = ctx.storage;
|
|
278
|
+
this.orezWorkerVersion = env.CF_VERSION?.id ?? '';
|
|
279
|
+
ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS ${readyTable} (id INTEGER PRIMARY KEY CHECK (id = 1), version TEXT NOT NULL)`);
|
|
280
|
+
ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS ${attemptTable} (id INTEGER PRIMARY KEY CHECK (id = 1), version TEXT NOT NULL, attempt_count INTEGER NOT NULL, last_error TEXT)`);
|
|
281
|
+
ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS ${restoreTable} (id INTEGER PRIMARY KEY CHECK (id = 1), started_at INTEGER NOT NULL)`);
|
|
282
|
+
}
|
|
283
|
+
applicationSqlDidCommit(_changed, mutated) {
|
|
284
|
+
if (mutated)
|
|
285
|
+
this.orezBumpBackupMarker();
|
|
286
|
+
}
|
|
287
|
+
async orezImportBatch(statements) {
|
|
288
|
+
if (statements.length === 0)
|
|
289
|
+
return;
|
|
290
|
+
await this.runApplicationTransaction(() => {
|
|
291
|
+
throw new Error('backup imports do not compile query ASTs');
|
|
292
|
+
}, async (tx) => {
|
|
293
|
+
for (const statement of statements) {
|
|
294
|
+
await tx.exec(statement.sql, statement.params ?? []);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async orezRunApplicationSchema(schemaVersion, instance, runOptions = {}) {
|
|
299
|
+
const client = this.applicationSqlLocalClient(instance);
|
|
300
|
+
const finishingRestore = this.orezRestoreInProgress();
|
|
301
|
+
if (finishingRestore) {
|
|
302
|
+
if (!runOptions.force) {
|
|
303
|
+
throw new Error('namespace restore is in progress; retry the restore or force schema reconciliation');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (this.orezSchemaRunVersion === schemaVersion && this.orezSchemaRun) {
|
|
307
|
+
return this.orezSchemaRun;
|
|
308
|
+
}
|
|
309
|
+
if (!runOptions.force && this.orezApplicationSchemaReady(schemaVersion)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!runOptions.force) {
|
|
313
|
+
const attemptKey = this.orezSchemaAttemptKey(schemaVersion);
|
|
314
|
+
const attempt = this.orezStorage.sql
|
|
315
|
+
.exec(`SELECT attempt_count, last_error FROM ${attemptTable} WHERE id = 1 AND version = ?`, attemptKey)
|
|
316
|
+
.toArray()[0];
|
|
317
|
+
if (attempt?.last_error &&
|
|
318
|
+
Number(attempt.attempt_count) >= suspendAfterFailures) {
|
|
319
|
+
throw new Error(`schema reconcile suspended after ${attempt.attempt_count} failed attempts for schema ${schemaVersion}; last error: ${attempt.last_error} — deploy a new worker or force a retry`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const attemptKey = this.orezSchemaAttemptKey(schemaVersion);
|
|
323
|
+
this.orezStorage.sql.exec(`INSERT INTO ${attemptTable} (id, version, attempt_count, last_error) VALUES (1, ?, 1, NULL) ON CONFLICT (id) DO UPDATE SET version = excluded.version, attempt_count = CASE WHEN version = excluded.version THEN attempt_count + 1 ELSE 1 END, last_error = CASE WHEN version = excluded.version THEN last_error ELSE NULL END`, attemptKey);
|
|
324
|
+
this.orezSchemaRunVersion = schemaVersion;
|
|
325
|
+
this.orezSchemaRun = (async () => {
|
|
326
|
+
try {
|
|
327
|
+
this.orezBeginApplicationSchemaReconcile();
|
|
328
|
+
const result = await options.schema.migrate({ client, instance });
|
|
329
|
+
if (finishingRestore) {
|
|
330
|
+
this.orezStorage.sql.exec(`DELETE FROM ${restoreTable} WHERE id = 1`);
|
|
331
|
+
}
|
|
332
|
+
this.orezMarkApplicationSchemaReady(schemaVersion);
|
|
333
|
+
this.orezStorage.sql.exec(`UPDATE ${attemptTable} SET last_error = NULL WHERE id = 1 AND version = ?`, attemptKey);
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
this.orezStorage.sql.exec(`UPDATE ${attemptTable} SET last_error = ? WHERE id = 1 AND version = ?`, errorMessage(error).slice(0, 4_000), attemptKey);
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
})();
|
|
341
|
+
this.orezSchemaRun.catch(() => {
|
|
342
|
+
this.orezSchemaRun = null;
|
|
343
|
+
this.orezSchemaRunVersion = null;
|
|
344
|
+
});
|
|
345
|
+
return this.orezSchemaRun;
|
|
346
|
+
}
|
|
347
|
+
orezStartApplicationSchema(schemaVersion, instance) {
|
|
348
|
+
const status = this.orezApplicationSchemaStatus(schemaVersion);
|
|
349
|
+
if (status.ready || status.running)
|
|
350
|
+
return status;
|
|
351
|
+
this.orezRunApplicationSchema(schemaVersion, instance).catch(() => { });
|
|
352
|
+
return this.orezApplicationSchemaStatus(schemaVersion);
|
|
353
|
+
}
|
|
354
|
+
orezApplicationSchemaStatus(schemaVersion) {
|
|
355
|
+
const ready = this.orezApplicationSchemaReady(schemaVersion);
|
|
356
|
+
const attemptKey = this.orezSchemaAttemptKey(schemaVersion);
|
|
357
|
+
const attempt = this.orezStorage.sql
|
|
358
|
+
.exec(`SELECT attempt_count, last_error FROM ${attemptTable} WHERE id = 1 AND version = ?`, attemptKey)
|
|
359
|
+
.toArray()[0];
|
|
360
|
+
return {
|
|
361
|
+
ready,
|
|
362
|
+
running: !ready &&
|
|
363
|
+
this.orezSchemaRunVersion === schemaVersion &&
|
|
364
|
+
this.orezSchemaRun !== null,
|
|
365
|
+
restoring: this.orezRestoreInProgress(),
|
|
366
|
+
attemptCount: Number(attempt?.attempt_count ?? 0),
|
|
367
|
+
lastError: attempt?.last_error ? String(attempt.last_error) : null,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
orezApplicationSchemaReady(schemaVersion) {
|
|
371
|
+
if (this.orezReadyVersion === schemaVersion)
|
|
372
|
+
return true;
|
|
373
|
+
const stored = this.orezStorage.sql
|
|
374
|
+
.exec(`SELECT version FROM ${readyTable} WHERE id = 1`)
|
|
375
|
+
.toArray()[0];
|
|
376
|
+
if (stored?.version !== schemaVersion)
|
|
377
|
+
return false;
|
|
378
|
+
this.orezReadyVersion = schemaVersion;
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
orezBeginApplicationSchemaReconcile() {
|
|
382
|
+
this.orezReadyVersion = null;
|
|
383
|
+
this.orezStorage.sql.exec(`DELETE FROM ${readyTable} WHERE id = 1`);
|
|
384
|
+
}
|
|
385
|
+
orezMarkApplicationSchemaReady(schemaVersion) {
|
|
386
|
+
this.orezStorage.sql.exec(`INSERT INTO ${readyTable} (id, version) VALUES (1, ?) ON CONFLICT (id) DO UPDATE SET version = excluded.version`, schemaVersion);
|
|
387
|
+
this.orezReadyVersion = schemaVersion;
|
|
388
|
+
}
|
|
389
|
+
orezBumpBackupMarker() {
|
|
390
|
+
this.orezStorage.sql.exec(`CREATE TABLE IF NOT EXISTS ${backupMarkerTable} (id INTEGER PRIMARY KEY CHECK (id = 1), write_seq INTEGER NOT NULL DEFAULT 0)`);
|
|
391
|
+
this.orezStorage.sql.exec(`INSERT INTO ${backupMarkerTable} (id, write_seq) VALUES (1, 1) ON CONFLICT (id) DO UPDATE SET write_seq = write_seq + 1`);
|
|
392
|
+
}
|
|
393
|
+
async orezBeginRestore() {
|
|
394
|
+
this.orezBeginApplicationSchemaReconcile();
|
|
395
|
+
this.orezStorage.sql.exec(`INSERT INTO ${restoreTable} (id, started_at) VALUES (1, ?) ON CONFLICT (id) DO UPDATE SET started_at = excluded.started_at`, Date.now());
|
|
396
|
+
}
|
|
397
|
+
orezRestoreInProgress() {
|
|
398
|
+
return Boolean(this.orezStorage.sql
|
|
399
|
+
.exec(`SELECT id FROM ${restoreTable} WHERE id = 1`)
|
|
400
|
+
.toArray()[0]);
|
|
401
|
+
}
|
|
402
|
+
orezSchemaAttemptKey(schemaVersion) {
|
|
403
|
+
return this.orezWorkerVersion
|
|
404
|
+
? `${schemaVersion}@${this.orezWorkerVersion}`
|
|
405
|
+
: schemaVersion;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const ensureNamespaceSchema = (env, namespace = 'singleton', runOptions = {}) => {
|
|
409
|
+
installRuntimeGlobals(env);
|
|
410
|
+
const instance = canonical(namespace);
|
|
411
|
+
const id = env.ZERO_SQL_DO.idFromName(instance);
|
|
412
|
+
return env.ZERO_SQL_DO.get(id).orezRunApplicationSchema(options.schema.version, instance, runOptions);
|
|
413
|
+
};
|
|
414
|
+
const queryNamespace = async (env, namespace, sql, params = []) => {
|
|
415
|
+
const client = applicationSqlClient(env, namespace);
|
|
416
|
+
if (/^\s*(?:SELECT|PRAGMA)\b/i.test(sql))
|
|
417
|
+
return client.query(sql, params);
|
|
418
|
+
await client.exec(sql, params);
|
|
419
|
+
return [];
|
|
420
|
+
};
|
|
421
|
+
const backupManager = options.backup
|
|
422
|
+
? createNamespaceBackupManager({
|
|
423
|
+
format: options.backup.format ?? 'orez-backup-v1',
|
|
424
|
+
markerTable: backupMarkerTable,
|
|
425
|
+
files: options.backup.bucket,
|
|
426
|
+
query: queryNamespace,
|
|
427
|
+
batch: async (env, namespace, statements) => {
|
|
428
|
+
const instance = canonical(namespace);
|
|
429
|
+
const id = env.ZERO_SQL_DO.idFromName(instance);
|
|
430
|
+
await env.ZERO_SQL_DO.get(id).orezImportBatch(statements);
|
|
431
|
+
},
|
|
432
|
+
listNamespaces: async (env) => {
|
|
433
|
+
const listed = await options.backup.inventory(applicationSqlClient(env, 'singleton'), env);
|
|
434
|
+
const namespaces = new Set(['singleton']);
|
|
435
|
+
for (const namespace of listed)
|
|
436
|
+
namespaces.add(canonical(namespace));
|
|
437
|
+
return [...namespaces];
|
|
438
|
+
},
|
|
439
|
+
logPrefix: `[${options.name}]`,
|
|
440
|
+
excludedTables: [
|
|
441
|
+
readyTable,
|
|
442
|
+
attemptTable,
|
|
443
|
+
backupMarkerTable,
|
|
444
|
+
restoreTable,
|
|
445
|
+
...(options.backup.excludedTables ?? []),
|
|
446
|
+
],
|
|
447
|
+
...Object.fromEntries(Object.entries(options.backup).filter(([key]) => [
|
|
448
|
+
'chunkTargetBytes',
|
|
449
|
+
'acceptedFormats',
|
|
450
|
+
'controlPlaneNamespace',
|
|
451
|
+
'keep',
|
|
452
|
+
'keepControlPlane',
|
|
453
|
+
'partBytes',
|
|
454
|
+
'prefix',
|
|
455
|
+
'runBudgetMs',
|
|
456
|
+
].includes(key))),
|
|
457
|
+
})
|
|
458
|
+
: null;
|
|
459
|
+
const feedResponse = async (request, env, resolved) => {
|
|
460
|
+
const isApplicationFeed = resolved.pathname === '/changes' || resolved.pathname === '/snapshot';
|
|
461
|
+
if (isApplicationFeed && options.schema.publicTables.length === 0) {
|
|
462
|
+
return new Response('Orez data feeds require a published application schema', {
|
|
463
|
+
status: 503,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
const id = env.ZERO_SQL_DO.idFromName(resolved.instance);
|
|
467
|
+
const stub = env.ZERO_SQL_DO.get(id);
|
|
468
|
+
if (isApplicationFeed) {
|
|
469
|
+
const status = await stub.orezApplicationSchemaStatus(options.schema.version);
|
|
470
|
+
if (!status.ready) {
|
|
471
|
+
const pending = await stub.orezStartApplicationSchema(options.schema.version, resolved.instance);
|
|
472
|
+
if (!pending.ready) {
|
|
473
|
+
return new Response(pending.lastError
|
|
474
|
+
? `${pending.lastError} (schema retry attempt ${pending.attemptCount} is in progress)`
|
|
475
|
+
: 'namespace schema migration in progress', { status: 503, headers: { 'retry-after': '10' } });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const headers = new Headers(request.headers);
|
|
480
|
+
headers.set('x-orez-do-instance', resolved.instance);
|
|
481
|
+
const snapshotTable = resolved.pathname === '/snapshot' ? resolved.url.searchParams.get('table') : null;
|
|
482
|
+
if (snapshotTable === 'syncCursor') {
|
|
483
|
+
const headUrl = new URL(resolved.url);
|
|
484
|
+
headUrl.pathname = '/changes';
|
|
485
|
+
headUrl.search = '';
|
|
486
|
+
headUrl.searchParams.set('watermark', String(Number.MAX_SAFE_INTEGER));
|
|
487
|
+
const headResponse = await stub.fetch(new Request(headUrl.toString(), {
|
|
488
|
+
headers,
|
|
489
|
+
signal: request.signal,
|
|
490
|
+
}));
|
|
491
|
+
if (!headResponse.ok)
|
|
492
|
+
return headResponse;
|
|
493
|
+
const head = await headResponse.json();
|
|
494
|
+
const watermark = isRecord(head) ? Number(head.watermark ?? 0) : 0;
|
|
495
|
+
return Response.json({
|
|
496
|
+
watermark,
|
|
497
|
+
rows: [{ id: 'zero-http', watermark }],
|
|
498
|
+
nextCursor: null,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const forward = new Request(resolved.url.toString(), request);
|
|
502
|
+
for (const [key, value] of headers)
|
|
503
|
+
forward.headers.set(key, value);
|
|
504
|
+
const response = await stub.fetch(forward);
|
|
505
|
+
if (!response.ok ||
|
|
506
|
+
(resolved.pathname !== '/changes' && resolved.pathname !== '/snapshot')) {
|
|
507
|
+
return response;
|
|
508
|
+
}
|
|
509
|
+
const body = projectFeedBody(feedTables, await response.json(), snapshotTable);
|
|
510
|
+
const responseHeaders = new Headers(response.headers);
|
|
511
|
+
responseHeaders.delete('content-length');
|
|
512
|
+
responseHeaders.set('content-type', 'application/json');
|
|
513
|
+
return new Response(JSON.stringify(body), {
|
|
514
|
+
status: response.status,
|
|
515
|
+
statusText: response.statusText,
|
|
516
|
+
headers: responseHeaders,
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
const reportError = (error, context) => {
|
|
520
|
+
if (!options.onError)
|
|
521
|
+
return;
|
|
522
|
+
try {
|
|
523
|
+
context.executionContext.waitUntil(Promise.resolve(options.onError(error, context)).catch(() => { }));
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// Observability must never replace the request's actual outcome.
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
const worker = {
|
|
530
|
+
async fetch(request, env, executionContext) {
|
|
531
|
+
const route = new URL(request.url).pathname;
|
|
532
|
+
try {
|
|
533
|
+
const response = await requestSignals.run(request.signal, async () => {
|
|
534
|
+
installRuntimeGlobals(env);
|
|
535
|
+
const resolved = resolveOrezDataRequest(request, namespaceOptions);
|
|
536
|
+
if (!resolved)
|
|
537
|
+
return new Response('invalid namespace', { status: 400 });
|
|
538
|
+
const routedHeaders = new Headers(request.headers);
|
|
539
|
+
routedHeaders.set(namespaceOptions.nsHeader, resolved.instance === 'singleton'
|
|
540
|
+
? 'singleton'
|
|
541
|
+
: resolved.instance.slice('ns:'.length));
|
|
542
|
+
const routedRequest = new Request(resolved.url.toString(), {
|
|
543
|
+
method: request.method,
|
|
544
|
+
headers: routedHeaders,
|
|
545
|
+
body: request.method === 'GET' || request.method === 'HEAD'
|
|
546
|
+
? undefined
|
|
547
|
+
: request.body,
|
|
548
|
+
redirect: request.redirect,
|
|
549
|
+
signal: request.signal,
|
|
550
|
+
});
|
|
551
|
+
const context = {
|
|
552
|
+
env,
|
|
553
|
+
executionContext,
|
|
554
|
+
instance: resolved.instance,
|
|
555
|
+
request: routedRequest,
|
|
556
|
+
url: resolved.url,
|
|
557
|
+
applicationSql: () => applicationSqlClient(env, resolved.instance, request.signal),
|
|
558
|
+
ensureSchema: (runOptions) => ensureNamespaceSchema(env, resolved.instance, runOptions),
|
|
559
|
+
};
|
|
560
|
+
await options.setup?.(context);
|
|
561
|
+
const custom = await options.routes?.(context);
|
|
562
|
+
if (custom)
|
|
563
|
+
return custom;
|
|
564
|
+
if (resolved.pathname === '/_orez/schema-status') {
|
|
565
|
+
const id = env.ZERO_SQL_DO.idFromName(resolved.instance);
|
|
566
|
+
const status = await env.ZERO_SQL_DO.get(id).orezApplicationSchemaStatus(options.schema.version);
|
|
567
|
+
return Response.json({
|
|
568
|
+
ns: resolved.instance,
|
|
569
|
+
objectId: id.toString(),
|
|
570
|
+
schemaVersion: options.schema.version,
|
|
571
|
+
...status,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
if (resolved.pathname === '/_orez/schema/migrate' &&
|
|
575
|
+
request.method === 'POST') {
|
|
576
|
+
await context.ensureSchema({
|
|
577
|
+
force: resolved.url.searchParams.get('force') === '1',
|
|
578
|
+
});
|
|
579
|
+
return Response.json({
|
|
580
|
+
ok: true,
|
|
581
|
+
ns: resolved.instance,
|
|
582
|
+
schemaVersion: options.schema.version,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if (resolved.pathname === '/changes' ||
|
|
586
|
+
resolved.pathname === '/snapshot' ||
|
|
587
|
+
resolved.pathname === '/_orez/write-budget' ||
|
|
588
|
+
resolved.pathname === '/_orez/write-budget/trip' ||
|
|
589
|
+
resolved.pathname === '/_orez/write-budget/reopen') {
|
|
590
|
+
return feedResponse(request, env, resolved);
|
|
591
|
+
}
|
|
592
|
+
if (backupManager &&
|
|
593
|
+
(resolved.pathname === '/_orez/backup/export' ||
|
|
594
|
+
resolved.pathname === '/_orez/backup/restore')) {
|
|
595
|
+
const action = resolved.pathname === '/_orez/backup/export' ? 'export' : 'restore';
|
|
596
|
+
if (!(await options.backup.authorize(request, env, action))) {
|
|
597
|
+
return new Response('forbidden', { status: 403 });
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
if (action === 'export') {
|
|
601
|
+
const summary = await backupManager.exportNamespace(env, resolved.instance);
|
|
602
|
+
await backupManager.pruneBackups(env, resolved.instance);
|
|
603
|
+
return Response.json({ ok: true, ...summary });
|
|
604
|
+
}
|
|
605
|
+
if (request.method !== 'POST') {
|
|
606
|
+
return new Response('restore requires POST', { status: 405 });
|
|
607
|
+
}
|
|
608
|
+
const confirmation = resolved.url.searchParams.get('confirm');
|
|
609
|
+
if (!confirmation || canonical(confirmation) !== resolved.instance) {
|
|
610
|
+
return new Response(`restore requires ?confirm=${resolved.instance}`, {
|
|
611
|
+
status: 400,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const body = await request.json().catch(() => null);
|
|
615
|
+
const key = isRecord(body) ? String(body.key ?? '') : '';
|
|
616
|
+
if (!key)
|
|
617
|
+
return new Response('restore requires a backup key', { status: 400 });
|
|
618
|
+
const id = env.ZERO_SQL_DO.idFromName(resolved.instance);
|
|
619
|
+
const stub = env.ZERO_SQL_DO.get(id);
|
|
620
|
+
await stub.orezBeginRestore();
|
|
621
|
+
const summary = await backupManager.importNamespace(env, resolved.instance, key);
|
|
622
|
+
await stub.orezRunApplicationSchema(options.schema.version, resolved.instance, { force: true });
|
|
623
|
+
return Response.json(summary);
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
return new Response(`${action} failed: ${errorMessage(error)}`, {
|
|
627
|
+
status: 500,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return new Response('orez data worker: not found', { status: 404 });
|
|
632
|
+
});
|
|
633
|
+
const isStandardRoute = STANDARD_DATA_PATHS.some((standardPath) => route === standardPath || route.endsWith(`/${standardPath.slice(1)}`));
|
|
634
|
+
let finalResponse = response;
|
|
635
|
+
if (isStandardRoute) {
|
|
636
|
+
const headers = new Headers(response.headers);
|
|
637
|
+
headers.set('x-orez-schema-version', options.schema.version);
|
|
638
|
+
finalResponse = new Response(response.body, {
|
|
639
|
+
status: response.status,
|
|
640
|
+
statusText: response.statusText,
|
|
641
|
+
headers,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (finalResponse.status >= 500 && options.onError) {
|
|
645
|
+
const error = new Error(`HTTP ${response.status} response from ${route}`);
|
|
646
|
+
error.name = 'HttpServerError';
|
|
647
|
+
reportError(error, { env, executionContext, route, request });
|
|
648
|
+
}
|
|
649
|
+
return finalResponse;
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
reportError(error, { env, executionContext, route, request });
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
async scheduled(event, env, ctx) {
|
|
657
|
+
installRuntimeGlobals(env);
|
|
658
|
+
const applicationHandler = options.scheduled?.[event.cron ?? ''];
|
|
659
|
+
if (applicationHandler) {
|
|
660
|
+
ctx.waitUntil(Promise.resolve(applicationHandler(event, env, ctx)).catch((error) => {
|
|
661
|
+
reportError(error, {
|
|
662
|
+
env,
|
|
663
|
+
executionContext: ctx,
|
|
664
|
+
route: `cron:${event.cron ?? ''}`,
|
|
665
|
+
event,
|
|
666
|
+
});
|
|
667
|
+
throw error;
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
else if (backupManager) {
|
|
671
|
+
ctx.waitUntil(backupManager.runScheduledBackups(env));
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
return {
|
|
676
|
+
ZeroSqlDO,
|
|
677
|
+
ZeroDO: ZeroSqlDO,
|
|
678
|
+
fetch: worker.fetch,
|
|
679
|
+
scheduled: worker.scheduled,
|
|
680
|
+
applicationSqlClient,
|
|
681
|
+
ensureNamespaceSchema,
|
|
682
|
+
backupManager,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
//# sourceMappingURL=lite-data-worker.js.map
|