@adep/runtime 0.1.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/README.md +37 -0
- package/dist/database/builder/dialect.d.ts +39 -0
- package/dist/database/builder/guards.d.ts +14 -0
- package/dist/database/builder/index.d.ts +16 -0
- package/dist/database/builder/owned.d.ts +67 -0
- package/dist/database/builder/table.d.ts +50 -0
- package/dist/database/builder/ulid.d.ts +12 -0
- package/dist/database/provision/identifier.d.ts +20 -0
- package/dist/database/sdk/cloud.d.ts +15 -0
- package/dist/db/project-driver.d.ts +59 -0
- package/dist/functions/deps/manifest.d.ts +33 -0
- package/dist/functions/deps/resolve.d.ts +20 -0
- package/dist/functions/domain.d.ts +28 -0
- package/dist/functions/runtime/executor.d.ts +18 -0
- package/dist/functions/runtime/worker-executor.d.ts +30 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +1397 -0
- package/dist/shared/capability-keys.d.ts +27 -0
- package/dist/shared/executor-runtime.d.ts +30 -0
- package/dist/shared/function-source.d.ts +44 -0
- package/dist/storage/cloud.d.ts +11 -0
- package/dist/storage/driver/local.d.ts +13 -0
- package/dist/storage/driver.d.ts +66 -0
- package/dist/storage/signature.d.ts +23 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
// packages/runtime/src/shared/executor-runtime.ts
|
|
2
|
+
var ExecutorError = class extends Error {
|
|
3
|
+
constructor(status, code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "ExecutorError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var TIMEOUT_CODE = "FN_EXEC_TIMEOUT";
|
|
11
|
+
var OOM_CODE = "FN_EXEC_OOM";
|
|
12
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
13
|
+
var DEFAULT_MEMORY_LIMIT_MB = 128;
|
|
14
|
+
function isHttpStatus(status) {
|
|
15
|
+
return typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599;
|
|
16
|
+
}
|
|
17
|
+
function normalizeHttpStatus(status) {
|
|
18
|
+
return isHttpStatus(status) ? status : 500;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// packages/runtime/src/shared/capability-keys.ts
|
|
22
|
+
var RPC_CAPABILITY_KEY = "__adepRpc";
|
|
23
|
+
var CHAIN_CAPABILITY_KEY = "__adepChain";
|
|
24
|
+
var DB_RPC = {
|
|
25
|
+
/** 直通方法(如 query):`(method=query, args=[sql, params])`。 */
|
|
26
|
+
query: "query",
|
|
27
|
+
/** 读取 owned 表变更流(DB-006):`(method=changes, args=[table, query])`。 */
|
|
28
|
+
changes: "changes",
|
|
29
|
+
/** 开启事务:`begin → txId`。 */
|
|
30
|
+
begin: "begin",
|
|
31
|
+
/** 提交事务:`commit, args=[txId]`。 */
|
|
32
|
+
commit: "commit",
|
|
33
|
+
/** 回滚事务:`rollback, args=[txId]`。 */
|
|
34
|
+
rollback: "rollback",
|
|
35
|
+
/** 执行一条链:`chain, args=[ChainRequest]`。 */
|
|
36
|
+
chain: "chain"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// packages/runtime/src/functions/runtime/worker-executor.ts
|
|
40
|
+
import { readFileSync } from "node:fs";
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
42
|
+
import { dirname, join } from "node:path";
|
|
43
|
+
import { Worker } from "node:worker_threads";
|
|
44
|
+
|
|
45
|
+
// packages/runtime/src/functions/deps/resolve.ts
|
|
46
|
+
import { resolve } from "node:path";
|
|
47
|
+
|
|
48
|
+
// packages/runtime/src/functions/deps/manifest.ts
|
|
49
|
+
import { createHash } from "node:crypto";
|
|
50
|
+
|
|
51
|
+
// packages/runtime/src/functions/domain.ts
|
|
52
|
+
var MAX_TOTAL_SOURCE_BYTES = 256 * 1024;
|
|
53
|
+
var FnError = class extends Error {
|
|
54
|
+
status;
|
|
55
|
+
code;
|
|
56
|
+
constructor(status, code, message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.name = "FnError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// packages/runtime/src/functions/deps/manifest.ts
|
|
65
|
+
var PACKAGE_NAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-._~]+$/;
|
|
66
|
+
var COMPARATOR_PATTERN = /^(?:\^|~|>=|<=|>|<|=)?\d+(?:\.(?:\d+|x|X|\*)){0,2}(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
67
|
+
var MANIFEST_MAX_DEPS = 100;
|
|
68
|
+
function isVersionRangeSpec(spec) {
|
|
69
|
+
if (spec === "*" || spec === "latest") return true;
|
|
70
|
+
const unions = spec.split("||");
|
|
71
|
+
return unions.every((union) => {
|
|
72
|
+
const trimmed = union.trim();
|
|
73
|
+
if (trimmed.length === 0) return false;
|
|
74
|
+
const hyphen = trimmed.split(" - ");
|
|
75
|
+
if (hyphen.length === 2) {
|
|
76
|
+
return COMPARATOR_PATTERN.test(hyphen[0].trim()) && COMPARATOR_PATTERN.test(hyphen[1].trim());
|
|
77
|
+
}
|
|
78
|
+
return trimmed.split(/\s+/).every((part) => COMPARATOR_PATTERN.test(part));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function parseManifestDependencies(content) {
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(content);
|
|
85
|
+
} catch {
|
|
86
|
+
throw new FnError(400, "DEP_INVALID_MANIFEST", "package.json \u4E0D\u662F\u5408\u6CD5 JSON");
|
|
87
|
+
}
|
|
88
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
89
|
+
throw new FnError(400, "DEP_INVALID_MANIFEST", "package.json \u9876\u5C42\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
90
|
+
}
|
|
91
|
+
const raw = parsed["dependencies"];
|
|
92
|
+
if (raw === void 0) return {};
|
|
93
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
94
|
+
throw new FnError(400, "DEP_INVALID_MANIFEST", "dependencies \u5FC5\u987B\u662F { \u5305\u540D: \u7248\u672C\u8303\u56F4 } \u5BF9\u8C61");
|
|
95
|
+
}
|
|
96
|
+
const entries = Object.entries(raw);
|
|
97
|
+
if (entries.length > MANIFEST_MAX_DEPS) {
|
|
98
|
+
throw new FnError(
|
|
99
|
+
400,
|
|
100
|
+
"DEP_INVALID_MANIFEST",
|
|
101
|
+
`\u4F9D\u8D56\u6761\u76EE\u8D85\u8FC7\u4E0A\u9650\uFF08${MANIFEST_MAX_DEPS} \u4E2A\uFF0C\u5F53\u524D ${entries.length} \u4E2A\uFF09`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const dependencies = {};
|
|
105
|
+
for (const [name, spec] of entries) {
|
|
106
|
+
if (!PACKAGE_NAME_PATTERN.test(name)) {
|
|
107
|
+
throw new FnError(
|
|
108
|
+
400,
|
|
109
|
+
"DEP_INVALID_MANIFEST",
|
|
110
|
+
`\u975E\u6CD5\u4F9D\u8D56\u540D "${name}"\uFF1A\u987B\u4E3A npm \u5305\u540D\uFF08scoped \u5F62\u5982 @scope/name\uFF09`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (typeof spec !== "string" || !isVersionRangeSpec(spec)) {
|
|
114
|
+
throw new FnError(
|
|
115
|
+
400,
|
|
116
|
+
"DEP_SPEC_REJECTED",
|
|
117
|
+
`\u4F9D\u8D56 "${name}" \u7684\u7248\u672C\u58F0\u660E "${String(spec)}" \u4E0D\u88AB\u63A5\u53D7\uFF1A\u53EA\u652F\u6301\u8BED\u4E49\u5316\u7248\u672C\u8303\u56F4\uFF08^/~/>=/\u7CBE\u786E\uFF09\uFF0CURL\u3001git\u3001file\u3001workspace \u53D6\u5305\u88AB registry \u767D\u540D\u5355\u62D2\u7EDD`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
dependencies[name] = spec;
|
|
121
|
+
}
|
|
122
|
+
return dependencies;
|
|
123
|
+
}
|
|
124
|
+
function tryParseManifestDependencies(content) {
|
|
125
|
+
if (content === void 0) return {};
|
|
126
|
+
try {
|
|
127
|
+
return parseManifestDependencies(content);
|
|
128
|
+
} catch {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function classifyDependencies(dependencies, builtin) {
|
|
133
|
+
const builtinDeps = {};
|
|
134
|
+
const customDeps = {};
|
|
135
|
+
for (const [name, spec] of Object.entries(dependencies)) {
|
|
136
|
+
if (builtin.includes(name)) builtinDeps[name] = spec;
|
|
137
|
+
else customDeps[name] = spec;
|
|
138
|
+
}
|
|
139
|
+
return { builtinDeps, customDeps };
|
|
140
|
+
}
|
|
141
|
+
function depsKeyOf(dependencies) {
|
|
142
|
+
const canonical = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`).toSorted().join("\n");
|
|
143
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// packages/runtime/src/functions/deps/resolve.ts
|
|
147
|
+
function resolveExecutionDeps(config, projectId, manifestContent) {
|
|
148
|
+
const manifest = tryParseManifestDependencies(manifestContent);
|
|
149
|
+
const { customDeps } = classifyDependencies(manifest, config.builtin);
|
|
150
|
+
const custom = Object.keys(customDeps);
|
|
151
|
+
return {
|
|
152
|
+
// 绝对化(相对进程 cwd,与 installer 侧 resolve(rootDir) 同基准):worker 的 createRequire 需要确定路径
|
|
153
|
+
dir: custom.length === 0 ? null : resolve(config.rootDir, projectId, depsKeyOf(customDeps)),
|
|
154
|
+
builtin: [...config.builtin],
|
|
155
|
+
custom
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// packages/runtime/src/functions/runtime/worker-executor.ts
|
|
160
|
+
function resolveWorkerEntry(baseDir = fileURLToPath(new URL(".", import.meta.url))) {
|
|
161
|
+
const candidates = [
|
|
162
|
+
join(baseDir, "worker-entry.js"),
|
|
163
|
+
join(baseDir, "worker-entry.ts"),
|
|
164
|
+
join(baseDir, "domains/functions/runtime/worker-entry.js"),
|
|
165
|
+
join(baseDir, "domains/functions/runtime/worker-entry.ts")
|
|
166
|
+
];
|
|
167
|
+
let current = baseDir;
|
|
168
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
169
|
+
candidates.push(
|
|
170
|
+
join(current, "packages/runtime/src/functions/runtime/worker-entry.js"),
|
|
171
|
+
join(current, "packages/runtime/src/functions/runtime/worker-entry.ts")
|
|
172
|
+
);
|
|
173
|
+
const parent = dirname(current);
|
|
174
|
+
if (parent === current) break;
|
|
175
|
+
current = parent;
|
|
176
|
+
}
|
|
177
|
+
for (const candidate of candidates) {
|
|
178
|
+
try {
|
|
179
|
+
readFileSync(candidate);
|
|
180
|
+
return pathToFileURL(candidate);
|
|
181
|
+
} catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D worker \u5165\u53E3\uFF08worker-entry.ts / worker-entry.js\uFF09");
|
|
186
|
+
}
|
|
187
|
+
function rpcReply(worker, id, ok, payload) {
|
|
188
|
+
worker.postMessage({
|
|
189
|
+
type: "rpc-response",
|
|
190
|
+
id,
|
|
191
|
+
ok,
|
|
192
|
+
...payload?.result === void 0 ? {} : { result: payload.result },
|
|
193
|
+
...payload?.error === void 0 ? {} : { error: payload.error }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
var WorkerFunctionExecutor = class {
|
|
197
|
+
depsConfig;
|
|
198
|
+
fetchConfig;
|
|
199
|
+
constructor(options = {}) {
|
|
200
|
+
this.depsConfig = options.deps;
|
|
201
|
+
this.fetchConfig = options.fetch;
|
|
202
|
+
}
|
|
203
|
+
async execute(input) {
|
|
204
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
205
|
+
const memoryLimitMb = input.memoryLimitMb ?? DEFAULT_MEMORY_LIMIT_MB;
|
|
206
|
+
const entry = input.entry ?? "index.ts";
|
|
207
|
+
const deps = this.depsConfig === void 0 ? void 0 : resolveExecutionDeps(this.depsConfig, input.project.id, input.files["package.json"]);
|
|
208
|
+
const logs = [];
|
|
209
|
+
return new Promise((resolve2, reject) => {
|
|
210
|
+
let worker;
|
|
211
|
+
try {
|
|
212
|
+
worker = new Worker(resolveWorkerEntry(), {
|
|
213
|
+
workerData: {
|
|
214
|
+
files: input.files,
|
|
215
|
+
entry,
|
|
216
|
+
request: input.request,
|
|
217
|
+
capabilities: input.capabilities ?? [],
|
|
218
|
+
...input.env === void 0 ? {} : { env: input.env },
|
|
219
|
+
...deps === void 0 ? {} : { deps },
|
|
220
|
+
...this.fetchConfig === void 0 ? {} : { fetch: this.fetchConfig }
|
|
221
|
+
},
|
|
222
|
+
resourceLimits: { maxOldGenerationSizeMb: memoryLimitMb }
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
reject(
|
|
226
|
+
new ExecutorError(
|
|
227
|
+
500,
|
|
228
|
+
"FN_EXEC_ERROR",
|
|
229
|
+
error instanceof Error ? error.message : "worker \u6784\u9020\u5931\u8D25"
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const timer = setTimeout(() => {
|
|
235
|
+
void worker.terminate();
|
|
236
|
+
reject(
|
|
237
|
+
new ExecutorError(504, TIMEOUT_CODE, `\u51FD\u6570\u6267\u884C\u8D85\u8FC7 ${timeoutMs}ms \u9650\u5236\uFF0Cworker \u5DF2\u88AB\u56DE\u6536`)
|
|
238
|
+
);
|
|
239
|
+
}, timeoutMs);
|
|
240
|
+
worker.on(
|
|
241
|
+
"message",
|
|
242
|
+
(message) => {
|
|
243
|
+
if (message.type === "rpc") {
|
|
244
|
+
const handler = input.rpcHandlers?.[message.capability ?? ""];
|
|
245
|
+
if (handler === void 0) {
|
|
246
|
+
rpcReply(worker, message.id, false, {
|
|
247
|
+
error: `RPC \u80FD\u529B "${message.capability ?? ""}" \u672A\u6CE8\u518C`
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const rpcArgs = Array.isArray(message.args) ? message.args : [];
|
|
252
|
+
void (async () => {
|
|
253
|
+
try {
|
|
254
|
+
const result = await handler(message.method ?? "", rpcArgs);
|
|
255
|
+
rpcReply(worker, message.id, true, { result });
|
|
256
|
+
} catch (error) {
|
|
257
|
+
rpcReply(worker, message.id, false, {
|
|
258
|
+
error: error instanceof Error ? error.message : String(error)
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (message.type === "log") {
|
|
265
|
+
logs.push(`[${message.level ?? "log"}] ${message.message ?? ""}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (message.type === "result") {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
void worker.terminate();
|
|
271
|
+
resolve2({ body: message.body, logs });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (message.type === "error") {
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
void worker.terminate();
|
|
277
|
+
const isOom = typeof message.message === "string" && /allocation\s+(failed|error)|out\s+of\s+memory|\bOOM\b/i.test(message.message);
|
|
278
|
+
reject(
|
|
279
|
+
isOom ? new ExecutorError(500, OOM_CODE, "\u51FD\u6570\u5185\u5B58\u8D85\u9650\uFF0C\u6267\u884C\u5DF2\u4E2D\u6B62") : (
|
|
280
|
+
// FN-011:函数抛错携带的 status 透传落 ExecutorError.status(非法/缺失 → 500 兼容旧形态)。
|
|
281
|
+
new ExecutorError(
|
|
282
|
+
normalizeHttpStatus(message.status),
|
|
283
|
+
message.code ?? "FN_EXEC_ERROR",
|
|
284
|
+
message.message ?? "\u51FD\u6570\u6267\u884C\u5931\u8D25"
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
worker.on("error", (error) => {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
const isOom = /allocation\s+(failed|error)|out\s+of\s+memory|\bOOM\b/i.test(error.message);
|
|
294
|
+
reject(
|
|
295
|
+
isOom ? new ExecutorError(500, OOM_CODE, "\u51FD\u6570\u5185\u5B58\u8D85\u9650\uFF0C\u6267\u884C\u5DF2\u4E2D\u6B62") : new ExecutorError(500, "FN_EXEC_ERROR", error.message)
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
async dispose() {
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// packages/runtime/src/database/sdk/cloud.ts
|
|
305
|
+
import { randomUUID } from "node:crypto";
|
|
306
|
+
|
|
307
|
+
// packages/runtime/src/database/builder/dialect.ts
|
|
308
|
+
var sqliteDialect = {
|
|
309
|
+
name: "sqlite",
|
|
310
|
+
placeholder: () => "?",
|
|
311
|
+
quote: (identifier) => `"${identifier.replace(/"/g, '""')}"`,
|
|
312
|
+
lastInsertIdClause: () => "",
|
|
313
|
+
upsertConflictClause: (columns) => ` ON CONFLICT (${columns.map((column) => `"${column.replace(/"/g, '""')}"`).join(", ")}) DO NOTHING`
|
|
314
|
+
};
|
|
315
|
+
var postgresDialect = {
|
|
316
|
+
name: "postgres",
|
|
317
|
+
placeholder: (index) => `$${index + 1}`,
|
|
318
|
+
quote: (identifier) => `"${identifier.replace(/"/g, '""')}"`,
|
|
319
|
+
lastInsertIdClause: () => " RETURNING id",
|
|
320
|
+
upsertConflictClause: (columns) => ` ON CONFLICT (${columns.map((column) => `"${column.replace(/"/g, '""')}"`).join(", ")}) DO NOTHING`
|
|
321
|
+
};
|
|
322
|
+
function dialectFor(driver) {
|
|
323
|
+
return driver.engine === "pg" ? postgresDialect : sqliteDialect;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// packages/runtime/src/database/provision/identifier.ts
|
|
327
|
+
var DbError = class extends Error {
|
|
328
|
+
constructor(status, code, message) {
|
|
329
|
+
super(message);
|
|
330
|
+
this.status = status;
|
|
331
|
+
this.code = code;
|
|
332
|
+
this.name = "DbError";
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
336
|
+
function assertIdentifier(name) {
|
|
337
|
+
if (!IDENTIFIER_PATTERN.test(name)) {
|
|
338
|
+
throw new DbError(
|
|
339
|
+
400,
|
|
340
|
+
"DB_UNSAFE_OP",
|
|
341
|
+
`\u975E\u6CD5\u6807\u8BC6\u7B26 "${name.slice(0, 32)}"\uFF1A\u4EC5\u5141\u8BB8\u5B57\u6BCD / \u4E0B\u5212\u7EBF\u5F00\u5934\u7684\u5B57\u6BCD\u6570\u5B57\u4E0B\u5212\u7EBF\u7EC4\u5408`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function quoteIdentifier(name) {
|
|
346
|
+
assertIdentifier(name);
|
|
347
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// packages/runtime/src/database/builder/guards.ts
|
|
351
|
+
function unsafeOperation(message) {
|
|
352
|
+
return new DbError(400, "DB_UNSAFE_OP", message);
|
|
353
|
+
}
|
|
354
|
+
function stripLiterals(sql) {
|
|
355
|
+
return sql.replace(/'[^']*(?:''[^']*)*'/g, "").replace(/"[^"]*(?:""[^"]*)*"/g, "").replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
356
|
+
}
|
|
357
|
+
function assertReadOnlyQuery(sql) {
|
|
358
|
+
const stripped = stripLiterals(sql);
|
|
359
|
+
if (stripped.includes(";")) {
|
|
360
|
+
throw unsafeOperation("\u4EC5\u5141\u8BB8\u5355\u6761\u8BED\u53E5\uFF0C\u7981\u6B62\u591A\u8BED\u53E5\u5806\u53E0");
|
|
361
|
+
}
|
|
362
|
+
const head = stripped.trim().replace(/^\(+/, "").trim().toUpperCase();
|
|
363
|
+
if (!head.startsWith("SELECT") && !head.startsWith("WITH")) {
|
|
364
|
+
throw unsafeOperation("cloud.db.query \u4EC5\u5141\u8BB8\u53EA\u8BFB\u67E5\u8BE2\uFF08SELECT / WITH\uFF09");
|
|
365
|
+
}
|
|
366
|
+
if (/ATTACH|DETACH/.test(stripped.toUpperCase())) {
|
|
367
|
+
throw unsafeOperation("\u7981\u6B62\u8DE8\u5E93\u64CD\u4F5C\uFF08ATTACH / DETACH\uFF09");
|
|
368
|
+
}
|
|
369
|
+
if (/SQLITE_\w+/i.test(stripped)) {
|
|
370
|
+
throw unsafeOperation("\u7981\u6B62\u8BBF\u95EE\u7CFB\u7EDF\u8868\uFF08sqlite_*\uFF09");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// packages/runtime/src/database/builder/owned.ts
|
|
375
|
+
var OWNED_PK = "id";
|
|
376
|
+
var OWNED_OWNER_KEY = "__owner_key";
|
|
377
|
+
var CHANGE_LOG_PREFIX = "_adep_changes_";
|
|
378
|
+
function changeLogTable(table) {
|
|
379
|
+
const name = `${CHANGE_LOG_PREFIX}${table}`;
|
|
380
|
+
assertIdentifier(name);
|
|
381
|
+
return name;
|
|
382
|
+
}
|
|
383
|
+
function assertUserColumn(name) {
|
|
384
|
+
assertIdentifier(name);
|
|
385
|
+
if (name.startsWith("__") && name !== OWNED_OWNER_KEY) {
|
|
386
|
+
throw new DbError(400, "DB_UNSAFE_OP", `\u4FDD\u7559\u524D\u7F00 "__" \u7684\u5217\u540D\u4E0D\u88AB\u5141\u8BB8\uFF1A${name.slice(0, 32)}`);
|
|
387
|
+
}
|
|
388
|
+
if (name.startsWith("_adep_")) {
|
|
389
|
+
throw new DbError(
|
|
390
|
+
400,
|
|
391
|
+
"DB_UNSAFE_OP",
|
|
392
|
+
`\u5E73\u53F0\u4FDD\u7559\u6BB5 "_adep_" \u7684\u5217\u540D\u4E0D\u88AB\u5141\u8BB8\uFF1A${name.slice(0, 32)}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function hasChangeLog(driver, table) {
|
|
397
|
+
if (driver.engine === "pg") {
|
|
398
|
+
return await driver.get(
|
|
399
|
+
"SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1",
|
|
400
|
+
[changeLogTable(table)]
|
|
401
|
+
) !== null;
|
|
402
|
+
}
|
|
403
|
+
return await driver.get("SELECT 1 AS x FROM sqlite_master WHERE type = ? AND name = ?", [
|
|
404
|
+
"table",
|
|
405
|
+
changeLogTable(table)
|
|
406
|
+
]) !== null;
|
|
407
|
+
}
|
|
408
|
+
async function assertOwned(driver, table) {
|
|
409
|
+
if (!await hasChangeLog(driver, table)) {
|
|
410
|
+
throw unsafeOperation(`\u8868 "${table}" \u4E0D\u662F owned \u8868\uFF08\u7F3A\u5C11\u53D8\u66F4\u6D41\u8868 ${changeLogTable(table)}\uFF09`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function appendChange(driver, table, op, id, ownerKey, before, after) {
|
|
414
|
+
const ph = dialectFor(driver).placeholder;
|
|
415
|
+
await driver.run(
|
|
416
|
+
`INSERT INTO ${quoteIdentifier(changeLogTable(table))} ("ts","op","id","__owner_key","before","after") VALUES (${ph(0)},${ph(1)},${ph(2)},${ph(3)},${ph(4)},${ph(5)})`,
|
|
417
|
+
[
|
|
418
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
419
|
+
op,
|
|
420
|
+
id,
|
|
421
|
+
ownerKey,
|
|
422
|
+
before === null ? null : JSON.stringify(before),
|
|
423
|
+
after === null ? null : JSON.stringify(after)
|
|
424
|
+
]
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
async function runOwnedWrite(driver, fn) {
|
|
428
|
+
await driver.run("SAVEPOINT _adep_change");
|
|
429
|
+
try {
|
|
430
|
+
await fn(driver);
|
|
431
|
+
await driver.run("RELEASE SAVEPOINT _adep_change");
|
|
432
|
+
} catch (error) {
|
|
433
|
+
await driver.run("ROLLBACK TO SAVEPOINT _adep_change");
|
|
434
|
+
await driver.run("RELEASE SAVEPOINT _adep_change");
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function parseChange(row) {
|
|
439
|
+
const before = row["before"];
|
|
440
|
+
const after = row["after"];
|
|
441
|
+
return {
|
|
442
|
+
seq: Number(row["seq"]),
|
|
443
|
+
ts: String(row["ts"]),
|
|
444
|
+
op: row["op"],
|
|
445
|
+
id: String(row["id"]),
|
|
446
|
+
ownerKey: row["__owner_key"] === null || row["__owner_key"] === void 0 ? null : String(row["__owner_key"]),
|
|
447
|
+
before: before === null || before === void 0 ? null : JSON.parse(String(before)),
|
|
448
|
+
after: after === null || after === void 0 ? null : JSON.parse(String(after))
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async function readChanges(driver, table, query = {}) {
|
|
452
|
+
await assertOwned(driver, table);
|
|
453
|
+
const ph = dialectFor(driver).placeholder;
|
|
454
|
+
const params = [];
|
|
455
|
+
const conditions = [];
|
|
456
|
+
if (query.afterSeq !== void 0) {
|
|
457
|
+
if (!Number.isInteger(query.afterSeq) || query.afterSeq < 0) {
|
|
458
|
+
throw unsafeOperation("afterSeq \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
459
|
+
}
|
|
460
|
+
conditions.push(`"seq" > ${ph(params.length)}`);
|
|
461
|
+
params.push(query.afterSeq);
|
|
462
|
+
}
|
|
463
|
+
if (query.ownerKey !== void 0) {
|
|
464
|
+
if (query.ownerKey === null) {
|
|
465
|
+
conditions.push('"__owner_key" IS NULL');
|
|
466
|
+
} else {
|
|
467
|
+
conditions.push(`"__owner_key" = ${ph(params.length)}`);
|
|
468
|
+
params.push(query.ownerKey);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (query.limit !== void 0) {
|
|
472
|
+
if (!Number.isInteger(query.limit) || query.limit < 0) {
|
|
473
|
+
throw unsafeOperation("limit \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
let sql = `SELECT "seq","ts","op","id","__owner_key","before","after" FROM ` + quoteIdentifier(changeLogTable(table));
|
|
477
|
+
if (conditions.length > 0) sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
478
|
+
sql += ' ORDER BY "seq" ASC';
|
|
479
|
+
if (query.limit !== void 0) {
|
|
480
|
+
sql += ` LIMIT ${ph(params.length)}`;
|
|
481
|
+
params.push(query.limit);
|
|
482
|
+
}
|
|
483
|
+
const rows = await driver.all(sql, params);
|
|
484
|
+
return rows.map(parseChange);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// packages/runtime/src/database/builder/ulid.ts
|
|
488
|
+
import { randomFillSync } from "node:crypto";
|
|
489
|
+
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
490
|
+
var TIME_LEN = 10;
|
|
491
|
+
var RANDOM_LEN = 16;
|
|
492
|
+
var lastTime = 0;
|
|
493
|
+
var lastRandom = "";
|
|
494
|
+
function encodeTime(now) {
|
|
495
|
+
let ts = Math.trunc(now);
|
|
496
|
+
let out = "";
|
|
497
|
+
for (let i = 0; i < TIME_LEN; i++) {
|
|
498
|
+
out = ENCODING[ts % 32] + out;
|
|
499
|
+
ts = Math.floor(ts / 32);
|
|
500
|
+
}
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
function encodeRandom(bytes) {
|
|
504
|
+
let out = "";
|
|
505
|
+
let buffer = 0;
|
|
506
|
+
let bits = 0;
|
|
507
|
+
for (const byte of bytes) {
|
|
508
|
+
buffer = buffer << 8 | byte;
|
|
509
|
+
bits += 8;
|
|
510
|
+
while (bits >= 5) {
|
|
511
|
+
out += ENCODING[buffer >>> bits - 5 & 31];
|
|
512
|
+
bits -= 5;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return out.slice(0, RANDOM_LEN);
|
|
516
|
+
}
|
|
517
|
+
function incrBase32(prev) {
|
|
518
|
+
const chars = prev.split("");
|
|
519
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
520
|
+
const idx = ENCODING.indexOf(chars[i]);
|
|
521
|
+
if (idx < 31) {
|
|
522
|
+
chars[i] = ENCODING[idx + 1];
|
|
523
|
+
return chars.join("");
|
|
524
|
+
}
|
|
525
|
+
chars[i] = ENCODING[0];
|
|
526
|
+
}
|
|
527
|
+
return chars.join("");
|
|
528
|
+
}
|
|
529
|
+
function ulid(now = Date.now()) {
|
|
530
|
+
const time = encodeTime(now);
|
|
531
|
+
let random;
|
|
532
|
+
if (now === lastTime) {
|
|
533
|
+
random = incrBase32(lastRandom);
|
|
534
|
+
} else {
|
|
535
|
+
lastTime = now;
|
|
536
|
+
const bytes = new Uint8Array(10);
|
|
537
|
+
randomFillSync(bytes);
|
|
538
|
+
random = encodeRandom(bytes);
|
|
539
|
+
}
|
|
540
|
+
lastRandom = random;
|
|
541
|
+
return time + random;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// packages/runtime/src/database/builder/table.ts
|
|
545
|
+
var OPERATORS = /* @__PURE__ */ new Set([
|
|
546
|
+
"=",
|
|
547
|
+
"!=",
|
|
548
|
+
">",
|
|
549
|
+
">=",
|
|
550
|
+
"<",
|
|
551
|
+
"<=",
|
|
552
|
+
"like",
|
|
553
|
+
"in",
|
|
554
|
+
"not in"
|
|
555
|
+
]);
|
|
556
|
+
var LIST_OPERATORS = /* @__PURE__ */ new Set(["in", "not in"]);
|
|
557
|
+
function pushParam(params, dialect, value) {
|
|
558
|
+
params.push(value);
|
|
559
|
+
return dialect.placeholder(params.length - 1);
|
|
560
|
+
}
|
|
561
|
+
function assertHasWhere(state) {
|
|
562
|
+
if (state.wheres.length === 0) {
|
|
563
|
+
throw unsafeOperation("update / delete \u5FC5\u987B\u5148\u6307\u5B9A where\uFF08\u9632\u6B62\u5168\u8868\u8BEF\u6539 / \u8BEF\u5220\uFF09");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async function resolveOwned(state, driver) {
|
|
567
|
+
if (state.owned === true) {
|
|
568
|
+
await assertOwned(driver, state.table);
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
return state.owned ?? await hasChangeLog(driver, state.table);
|
|
572
|
+
}
|
|
573
|
+
function toOwnerKey(row) {
|
|
574
|
+
const value = row[OWNED_OWNER_KEY];
|
|
575
|
+
return value === null || value === void 0 ? null : String(value);
|
|
576
|
+
}
|
|
577
|
+
function ownedInsertRow(row) {
|
|
578
|
+
const effective = { ...row };
|
|
579
|
+
if (effective[OWNED_PK] === void 0) {
|
|
580
|
+
effective[OWNED_PK] = ulid();
|
|
581
|
+
}
|
|
582
|
+
return effective;
|
|
583
|
+
}
|
|
584
|
+
async function captureBeforeRows(state, dialect, driver) {
|
|
585
|
+
const select = compileSelect({ ...state, columns: [] }, dialect);
|
|
586
|
+
return driver.all(select.sql, select.params);
|
|
587
|
+
}
|
|
588
|
+
function compileWhere(wheres, dialect, params) {
|
|
589
|
+
return wheres.map((where) => {
|
|
590
|
+
const quoted = dialect.quote(where.column);
|
|
591
|
+
if (LIST_OPERATORS.has(where.operator)) {
|
|
592
|
+
const values = where.value;
|
|
593
|
+
if (values.length === 0) {
|
|
594
|
+
throw unsafeOperation(`${where.operator.toUpperCase()} \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u503C`);
|
|
595
|
+
}
|
|
596
|
+
const marks = values.map((value2) => pushParam(params, dialect, value2));
|
|
597
|
+
return `${quoted} ${where.operator.toUpperCase()} (${marks.join(", ")})`;
|
|
598
|
+
}
|
|
599
|
+
const value = where.value;
|
|
600
|
+
if (value === null) {
|
|
601
|
+
if (where.operator === "=") return `${quoted} IS NULL`;
|
|
602
|
+
if (where.operator === "!=") return `${quoted} IS NOT NULL`;
|
|
603
|
+
}
|
|
604
|
+
const mark = pushParam(params, dialect, value);
|
|
605
|
+
return `${quoted} ${where.operator} ${mark}`;
|
|
606
|
+
}).join(" AND ");
|
|
607
|
+
}
|
|
608
|
+
function compileSelect(state, dialect) {
|
|
609
|
+
const params = [];
|
|
610
|
+
const columns = state.columns.length > 0 ? state.columns.map((column) => dialect.quote(column)).join(", ") : "*";
|
|
611
|
+
let sql = `SELECT ${columns} FROM ${dialect.quote(state.table)}`;
|
|
612
|
+
if (state.wheres.length > 0) {
|
|
613
|
+
sql += ` WHERE ${compileWhere(state.wheres, dialect, params)}`;
|
|
614
|
+
}
|
|
615
|
+
if (state.orderBys.length > 0) {
|
|
616
|
+
sql += ` ORDER BY ${state.orderBys.map((order) => `${dialect.quote(order.column)} ${order.direction.toUpperCase()}`).join(", ")}`;
|
|
617
|
+
}
|
|
618
|
+
if (state.limit !== void 0) sql += ` LIMIT ${pushParam(params, dialect, state.limit)}`;
|
|
619
|
+
if (state.offset !== void 0) sql += ` OFFSET ${pushParam(params, dialect, state.offset)}`;
|
|
620
|
+
return { sql, params };
|
|
621
|
+
}
|
|
622
|
+
function compileCount(state, dialect) {
|
|
623
|
+
const params = [];
|
|
624
|
+
let sql = `SELECT count(*) AS n FROM ${dialect.quote(state.table)}`;
|
|
625
|
+
if (state.wheres.length > 0) {
|
|
626
|
+
sql += ` WHERE ${compileWhere(state.wheres, dialect, params)}`;
|
|
627
|
+
}
|
|
628
|
+
return { sql, params };
|
|
629
|
+
}
|
|
630
|
+
function compileInsert(table, row, dialect) {
|
|
631
|
+
const entries = Object.entries(row);
|
|
632
|
+
if (entries.length === 0) {
|
|
633
|
+
throw unsafeOperation("insert \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u5B57\u6BB5");
|
|
634
|
+
}
|
|
635
|
+
const params = [];
|
|
636
|
+
const columns = entries.map(([name]) => {
|
|
637
|
+
assertUserColumn(name);
|
|
638
|
+
return dialect.quote(name);
|
|
639
|
+
}).join(", ");
|
|
640
|
+
const marks = entries.map(([, value]) => pushParam(params, dialect, value));
|
|
641
|
+
return {
|
|
642
|
+
sql: `INSERT INTO ${dialect.quote(table)} (${columns}) VALUES (${marks.join(", ")})`,
|
|
643
|
+
params
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function compileInsertMany(table, rows, dialect) {
|
|
647
|
+
if (rows.length === 0) {
|
|
648
|
+
throw unsafeOperation("insertMany \u81F3\u5C11\u9700\u8981\u4E00\u884C");
|
|
649
|
+
}
|
|
650
|
+
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
651
|
+
if (columns.length === 0) {
|
|
652
|
+
throw unsafeOperation("insertMany \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u5B57\u6BB5");
|
|
653
|
+
}
|
|
654
|
+
columns.forEach(assertUserColumn);
|
|
655
|
+
const params = [];
|
|
656
|
+
const quoted = columns.map((column) => dialect.quote(column)).join(", ");
|
|
657
|
+
const valueGroups = rows.map((row) => {
|
|
658
|
+
const marks = columns.map((column) => pushParam(params, dialect, row[column] ?? null));
|
|
659
|
+
return `(${marks.join(", ")})`;
|
|
660
|
+
});
|
|
661
|
+
return {
|
|
662
|
+
sql: `INSERT INTO ${dialect.quote(table)} (${quoted}) VALUES ${valueGroups.join(", ")}`,
|
|
663
|
+
params
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function compileUpdate(table, wheres, values, dialect) {
|
|
667
|
+
const entries = Object.entries(values);
|
|
668
|
+
if (entries.length === 0) {
|
|
669
|
+
throw unsafeOperation("update \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u5B57\u6BB5");
|
|
670
|
+
}
|
|
671
|
+
const params = [];
|
|
672
|
+
const sets = entries.map(([name, value]) => {
|
|
673
|
+
assertUserColumn(name);
|
|
674
|
+
return `${dialect.quote(name)} = ${pushParam(params, dialect, value)}`;
|
|
675
|
+
}).join(", ");
|
|
676
|
+
const where = compileWhere(wheres, dialect, params);
|
|
677
|
+
return { sql: `UPDATE ${dialect.quote(table)} SET ${sets} WHERE ${where}`, params };
|
|
678
|
+
}
|
|
679
|
+
function compileDelete(table, wheres, dialect) {
|
|
680
|
+
const params = [];
|
|
681
|
+
const where = compileWhere(wheres, dialect, params);
|
|
682
|
+
return { sql: `DELETE FROM ${dialect.quote(table)} WHERE ${where}`, params };
|
|
683
|
+
}
|
|
684
|
+
function createTableBuilder(table, driver, dialect, options = {}) {
|
|
685
|
+
assertIdentifier(table);
|
|
686
|
+
const guardWrite = options.guardWrite ?? (() => void 0);
|
|
687
|
+
const initialState = {
|
|
688
|
+
table,
|
|
689
|
+
columns: [],
|
|
690
|
+
wheres: [],
|
|
691
|
+
orderBys: [],
|
|
692
|
+
limit: void 0,
|
|
693
|
+
offset: void 0
|
|
694
|
+
};
|
|
695
|
+
const build = (state) => {
|
|
696
|
+
const derived = (patch) => build({ ...state, ...patch });
|
|
697
|
+
return {
|
|
698
|
+
select(...columns) {
|
|
699
|
+
columns.forEach(assertUserColumn);
|
|
700
|
+
return derived({ columns });
|
|
701
|
+
},
|
|
702
|
+
owned() {
|
|
703
|
+
return derived({ owned: true });
|
|
704
|
+
},
|
|
705
|
+
where(column, operatorOrValue, maybeValue) {
|
|
706
|
+
assertUserColumn(column);
|
|
707
|
+
let operator;
|
|
708
|
+
let value;
|
|
709
|
+
if (maybeValue !== void 0) {
|
|
710
|
+
operator = operatorOrValue;
|
|
711
|
+
if (!OPERATORS.has(operator)) {
|
|
712
|
+
throw unsafeOperation(`\u4E0D\u652F\u6301\u7684\u64CD\u4F5C\u7B26 "${operator}"`);
|
|
713
|
+
}
|
|
714
|
+
if (LIST_OPERATORS.has(operator) && !Array.isArray(maybeValue)) {
|
|
715
|
+
throw unsafeOperation(`${operator.toUpperCase()} \u9700\u8981\u6570\u7EC4\u503C`);
|
|
716
|
+
}
|
|
717
|
+
value = maybeValue;
|
|
718
|
+
} else {
|
|
719
|
+
operator = "=";
|
|
720
|
+
value = operatorOrValue;
|
|
721
|
+
}
|
|
722
|
+
return derived({ wheres: [...state.wheres, { column, operator, value }] });
|
|
723
|
+
},
|
|
724
|
+
orderBy(column, direction = "asc") {
|
|
725
|
+
assertUserColumn(column);
|
|
726
|
+
if (direction !== "asc" && direction !== "desc") {
|
|
727
|
+
throw unsafeOperation("orderBy \u65B9\u5411\u4EC5\u652F\u6301 asc / desc");
|
|
728
|
+
}
|
|
729
|
+
return derived({ orderBys: [...state.orderBys, { column, direction }] });
|
|
730
|
+
},
|
|
731
|
+
limit(n) {
|
|
732
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
733
|
+
throw unsafeOperation("limit \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
734
|
+
}
|
|
735
|
+
return derived({ limit: n });
|
|
736
|
+
},
|
|
737
|
+
offset(n) {
|
|
738
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
739
|
+
throw unsafeOperation("offset \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
740
|
+
}
|
|
741
|
+
return derived({ offset: n });
|
|
742
|
+
},
|
|
743
|
+
async get() {
|
|
744
|
+
const compiled = compileSelect(state, dialect);
|
|
745
|
+
return driver.all(compiled.sql, compiled.params);
|
|
746
|
+
},
|
|
747
|
+
async first() {
|
|
748
|
+
const compiled = compileSelect({ ...state, limit: 1 }, dialect);
|
|
749
|
+
return driver.get(compiled.sql, compiled.params);
|
|
750
|
+
},
|
|
751
|
+
async count() {
|
|
752
|
+
const compiled = compileCount(state, dialect);
|
|
753
|
+
const row = await driver.get(compiled.sql, compiled.params);
|
|
754
|
+
return row === null ? 0 : Number(row["n"]);
|
|
755
|
+
},
|
|
756
|
+
async insert(row) {
|
|
757
|
+
guardWrite();
|
|
758
|
+
Object.keys(row).forEach(assertUserColumn);
|
|
759
|
+
if (await resolveOwned(state, driver)) {
|
|
760
|
+
const effective = ownedInsertRow(row);
|
|
761
|
+
const ownerKey = toOwnerKey(effective);
|
|
762
|
+
const compiled2 = compileInsert(state.table, effective, dialect);
|
|
763
|
+
await runOwnedWrite(driver, async (d) => {
|
|
764
|
+
await d.run(compiled2.sql, compiled2.params);
|
|
765
|
+
await appendChange(
|
|
766
|
+
d,
|
|
767
|
+
state.table,
|
|
768
|
+
"insert",
|
|
769
|
+
String(effective[OWNED_PK]),
|
|
770
|
+
ownerKey,
|
|
771
|
+
null,
|
|
772
|
+
effective
|
|
773
|
+
);
|
|
774
|
+
});
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const compiled = compileInsert(state.table, row, dialect);
|
|
778
|
+
await driver.run(compiled.sql, compiled.params);
|
|
779
|
+
},
|
|
780
|
+
async insertMany(rows) {
|
|
781
|
+
guardWrite();
|
|
782
|
+
const insertColumns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
783
|
+
insertColumns.forEach(assertUserColumn);
|
|
784
|
+
if (await resolveOwned(state, driver)) {
|
|
785
|
+
const effectiveRows = rows.map(ownedInsertRow);
|
|
786
|
+
const compiled2 = compileInsertMany(state.table, effectiveRows, dialect);
|
|
787
|
+
await runOwnedWrite(driver, async (d) => {
|
|
788
|
+
await d.run(compiled2.sql, compiled2.params);
|
|
789
|
+
for (const effective of effectiveRows) {
|
|
790
|
+
await appendChange(
|
|
791
|
+
d,
|
|
792
|
+
state.table,
|
|
793
|
+
"insert",
|
|
794
|
+
String(effective[OWNED_PK]),
|
|
795
|
+
toOwnerKey(effective),
|
|
796
|
+
null,
|
|
797
|
+
effective
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const compiled = compileInsertMany(state.table, rows, dialect);
|
|
804
|
+
await driver.run(compiled.sql, compiled.params);
|
|
805
|
+
},
|
|
806
|
+
async update(values) {
|
|
807
|
+
guardWrite();
|
|
808
|
+
assertHasWhere(state);
|
|
809
|
+
Object.keys(values).forEach(assertUserColumn);
|
|
810
|
+
if (await resolveOwned(state, driver)) {
|
|
811
|
+
const before = await captureBeforeRows(state, dialect, driver);
|
|
812
|
+
const compiled2 = compileUpdate(state.table, state.wheres, values, dialect);
|
|
813
|
+
await runOwnedWrite(driver, async (d) => {
|
|
814
|
+
await d.run(compiled2.sql, compiled2.params);
|
|
815
|
+
for (const row of before) {
|
|
816
|
+
const after = { ...row, ...values };
|
|
817
|
+
await appendChange(
|
|
818
|
+
d,
|
|
819
|
+
state.table,
|
|
820
|
+
"update",
|
|
821
|
+
String(row[OWNED_PK]),
|
|
822
|
+
toOwnerKey(row),
|
|
823
|
+
row,
|
|
824
|
+
after
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
return before.length;
|
|
829
|
+
}
|
|
830
|
+
const compiled = compileUpdate(state.table, state.wheres, values, dialect);
|
|
831
|
+
return (await driver.run(compiled.sql, compiled.params)).changes;
|
|
832
|
+
},
|
|
833
|
+
async delete() {
|
|
834
|
+
guardWrite();
|
|
835
|
+
assertHasWhere(state);
|
|
836
|
+
if (await resolveOwned(state, driver)) {
|
|
837
|
+
const before = await captureBeforeRows(state, dialect, driver);
|
|
838
|
+
const compiled2 = compileDelete(state.table, state.wheres, dialect);
|
|
839
|
+
await runOwnedWrite(driver, async (d) => {
|
|
840
|
+
await d.run(compiled2.sql, compiled2.params);
|
|
841
|
+
for (const row of before) {
|
|
842
|
+
await appendChange(
|
|
843
|
+
d,
|
|
844
|
+
state.table,
|
|
845
|
+
"delete",
|
|
846
|
+
String(row[OWNED_PK]),
|
|
847
|
+
toOwnerKey(row),
|
|
848
|
+
row,
|
|
849
|
+
null
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
return before.length;
|
|
854
|
+
}
|
|
855
|
+
const compiled = compileDelete(state.table, state.wheres, dialect);
|
|
856
|
+
return (await driver.run(compiled.sql, compiled.params)).changes;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
};
|
|
860
|
+
return build(initialState);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// packages/runtime/src/database/builder/index.ts
|
|
864
|
+
var TX_STATES = /* @__PURE__ */ new WeakMap();
|
|
865
|
+
function createCloudDb(driver, options = {}) {
|
|
866
|
+
const dialect = options.dialect ?? dialectFor(driver);
|
|
867
|
+
let txState = TX_STATES.get(driver);
|
|
868
|
+
if (txState === void 0) {
|
|
869
|
+
txState = { inTransaction: false };
|
|
870
|
+
TX_STATES.set(driver, txState);
|
|
871
|
+
}
|
|
872
|
+
const makeHandle = (handleOptions) => {
|
|
873
|
+
const guardWrite = () => {
|
|
874
|
+
if (txState !== void 0 && txState.inTransaction && !handleOptions.allowWriteDuringTx) {
|
|
875
|
+
throw unsafeOperation("\u4E8B\u52A1\u8FDB\u884C\u4E2D\uFF1A\u7981\u6B62\u5728\u4E8B\u52A1\u5916\u5BF9\u540C\u4E00\u9879\u76EE\u5E93\u6267\u884C\u5199\u64CD\u4F5C");
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
const handle = {
|
|
879
|
+
table(name) {
|
|
880
|
+
return createTableBuilder(name, driver, dialect, { guardWrite });
|
|
881
|
+
},
|
|
882
|
+
async transaction(fn) {
|
|
883
|
+
if (txState !== void 0 && txState.inTransaction) {
|
|
884
|
+
throw unsafeOperation("\u4E0D\u5141\u8BB8\u5D4C\u5957\u4E8B\u52A1\uFF1A\u4E8B\u52A1\u5185\u7981\u6B62\u518D\u6B21\u8C03\u7528 transaction()");
|
|
885
|
+
}
|
|
886
|
+
txState.inTransaction = true;
|
|
887
|
+
await driver.run("BEGIN");
|
|
888
|
+
const txHandle = makeHandle({ allowWriteDuringTx: true });
|
|
889
|
+
try {
|
|
890
|
+
const result = await fn(txHandle);
|
|
891
|
+
await driver.run("COMMIT");
|
|
892
|
+
return result;
|
|
893
|
+
} catch (error) {
|
|
894
|
+
await driver.run("ROLLBACK");
|
|
895
|
+
throw error;
|
|
896
|
+
} finally {
|
|
897
|
+
txState.inTransaction = false;
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
async query(sql, params) {
|
|
901
|
+
if (!Array.isArray(params)) {
|
|
902
|
+
throw unsafeOperation("query \u7684 params \u5FC5\u987B\u63D0\u4F9B\uFF08\u65E0\u53C2\u4F20 []\uFF09");
|
|
903
|
+
}
|
|
904
|
+
guardWrite();
|
|
905
|
+
assertReadOnlyQuery(sql);
|
|
906
|
+
return driver.all(sql, params);
|
|
907
|
+
},
|
|
908
|
+
async changes(table, query = {}) {
|
|
909
|
+
return readChanges(driver, table, query);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
return handle;
|
|
913
|
+
};
|
|
914
|
+
return makeHandle({ allowWriteDuringTx: false });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// packages/runtime/src/database/sdk/cloud.ts
|
|
918
|
+
var CLOUD_DB_SPEC = {
|
|
919
|
+
kind: "cloud-db",
|
|
920
|
+
rootMethod: "table",
|
|
921
|
+
stepMethods: ["select", "where", "orderBy", "limit", "offset"],
|
|
922
|
+
terminalMethods: ["get", "first", "count", "insert", "insertMany", "update", "delete"],
|
|
923
|
+
directMethods: ["query", "changes"],
|
|
924
|
+
transactionMethod: "transaction"
|
|
925
|
+
};
|
|
926
|
+
var WRITE_TERMINALS = /* @__PURE__ */ new Set(["insert", "insertMany", "update", "delete"]);
|
|
927
|
+
function errorWithCode(error) {
|
|
928
|
+
const code = typeof error === "object" && error !== null && typeof error.code === "string" ? error.code : void 0;
|
|
929
|
+
if (code !== void 0 && error instanceof Error) {
|
|
930
|
+
return new Error(`[${code}] ${error.message}`);
|
|
931
|
+
}
|
|
932
|
+
return error;
|
|
933
|
+
}
|
|
934
|
+
function createDbCapability(driver, options = {}) {
|
|
935
|
+
const db = createCloudDb(driver);
|
|
936
|
+
const activeTxs = /* @__PURE__ */ new Map();
|
|
937
|
+
const guardTxWrite = (txId, terminal) => {
|
|
938
|
+
if (txId === void 0) {
|
|
939
|
+
if (activeTxs.size > 0 && WRITE_TERMINALS.has(terminal)) {
|
|
940
|
+
throw unsafeOperation("\u4E8B\u52A1\u8FDB\u884C\u4E2D\uFF1A\u7981\u6B62\u5728\u4E8B\u52A1\u5916\u5BF9\u540C\u4E00\u9879\u76EE\u5E93\u6267\u884C\u5199\u64CD\u4F5C");
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (!activeTxs.has(txId)) {
|
|
945
|
+
throw unsafeOperation("\u5F15\u7528\u7684\u4E8B\u52A1\u5DF2\u7ED3\u675F\u6216\u4E0D\u5B58\u5728");
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
const runChain = async (request) => {
|
|
949
|
+
const tableName = request.rootArgs[0];
|
|
950
|
+
if (typeof tableName !== "string" && typeof tableName !== "number") {
|
|
951
|
+
throw unsafeOperation("table \u9700\u8981\u8868\u540D\u5B57\u7B26\u4E32");
|
|
952
|
+
}
|
|
953
|
+
let builder = db.table(String(tableName));
|
|
954
|
+
for (const step of request.steps) {
|
|
955
|
+
const invoke = builder[step.method];
|
|
956
|
+
if (typeof invoke !== "function") {
|
|
957
|
+
throw unsafeOperation(`\u4E91\u6570\u636E\u5E93\u4E0D\u652F\u6301\u94FE\u5F0F\u65B9\u6CD5 "${String(step.method)}"`);
|
|
958
|
+
}
|
|
959
|
+
builder = invoke(...step.args);
|
|
960
|
+
}
|
|
961
|
+
guardTxWrite(request.txId, request.terminal);
|
|
962
|
+
if (WRITE_TERMINALS.has(request.terminal) && options.beforeWrite !== void 0) {
|
|
963
|
+
await options.beforeWrite(driver);
|
|
964
|
+
}
|
|
965
|
+
const terminal = builder[request.terminal];
|
|
966
|
+
if (typeof terminal !== "function") {
|
|
967
|
+
throw unsafeOperation(`\u4E91\u6570\u636E\u5E93\u4E0D\u652F\u6301\u7EC8\u503C\u65B9\u6CD5 "${request.terminal}"`);
|
|
968
|
+
}
|
|
969
|
+
return terminal(...request.terminalArgs);
|
|
970
|
+
};
|
|
971
|
+
const handler = async (method, args) => {
|
|
972
|
+
try {
|
|
973
|
+
switch (method) {
|
|
974
|
+
case DB_RPC.query: {
|
|
975
|
+
const [sql, params] = args;
|
|
976
|
+
return await db.query(sql, params);
|
|
977
|
+
}
|
|
978
|
+
case DB_RPC.changes: {
|
|
979
|
+
const [table, query] = args;
|
|
980
|
+
return await db.changes(table, query);
|
|
981
|
+
}
|
|
982
|
+
case DB_RPC.begin: {
|
|
983
|
+
if (activeTxs.size > 0) throw unsafeOperation("\u4E0D\u5141\u8BB8\u5D4C\u5957\u4E8B\u52A1");
|
|
984
|
+
await driver.run("BEGIN");
|
|
985
|
+
const txId = randomUUID();
|
|
986
|
+
activeTxs.set(txId, true);
|
|
987
|
+
return txId;
|
|
988
|
+
}
|
|
989
|
+
case DB_RPC.commit: {
|
|
990
|
+
const [txId] = args;
|
|
991
|
+
if (!activeTxs.has(txId)) throw unsafeOperation("\u5F15\u7528\u7684\u4E8B\u52A1\u5DF2\u7ED3\u675F\u6216\u4E0D\u5B58\u5728");
|
|
992
|
+
activeTxs.delete(txId);
|
|
993
|
+
await driver.run("COMMIT");
|
|
994
|
+
return void 0;
|
|
995
|
+
}
|
|
996
|
+
case DB_RPC.rollback: {
|
|
997
|
+
const [txId] = args;
|
|
998
|
+
if (activeTxs.has(txId)) {
|
|
999
|
+
activeTxs.delete(txId);
|
|
1000
|
+
await driver.run("ROLLBACK");
|
|
1001
|
+
}
|
|
1002
|
+
return void 0;
|
|
1003
|
+
}
|
|
1004
|
+
case DB_RPC.chain: {
|
|
1005
|
+
const request = args[0];
|
|
1006
|
+
return await runChain(request);
|
|
1007
|
+
}
|
|
1008
|
+
default:
|
|
1009
|
+
throw new Error(`\u672A\u77E5\u7684 cloud.db \u65B9\u6CD5 "${String(method)}"`);
|
|
1010
|
+
}
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
throw errorWithCode(error);
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
return {
|
|
1016
|
+
capabilities: [{ name: "db", value: { [CHAIN_CAPABILITY_KEY]: CLOUD_DB_SPEC } }],
|
|
1017
|
+
rpcHandlers: { db: handler }
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// packages/runtime/src/storage/driver.ts
|
|
1022
|
+
var PROJECT_QUOTA_BYTES = 1024 * 1024 * 1024;
|
|
1023
|
+
var StorageError = class extends Error {
|
|
1024
|
+
constructor(status, code, message) {
|
|
1025
|
+
super(message);
|
|
1026
|
+
this.status = status;
|
|
1027
|
+
this.code = code;
|
|
1028
|
+
this.name = "StorageError";
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
var STORAGE_CODES = {
|
|
1032
|
+
invalidPath: "STORAGE_INVALID_PATH",
|
|
1033
|
+
notFound: "STORAGE_NOT_FOUND",
|
|
1034
|
+
quotaExceeded: "STORAGE_QUOTA_EXCEEDED"
|
|
1035
|
+
};
|
|
1036
|
+
function assertSafeStoragePath(path) {
|
|
1037
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1038
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u5B58\u50A8\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1039
|
+
}
|
|
1040
|
+
if (path.length > 1024) {
|
|
1041
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u5B58\u50A8\u8DEF\u5F84\u8FC7\u957F\uFF08\u4E0A\u9650 1024 \u5B57\u7B26\uFF09");
|
|
1042
|
+
}
|
|
1043
|
+
if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || /[\0\r\n]/.test(path)) {
|
|
1044
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u5B58\u50A8\u8DEF\u5F84\u5FC5\u987B\u662F\u9879\u76EE\u6876\u5185\u7684\u76F8\u5BF9\u8DEF\u5F84");
|
|
1045
|
+
}
|
|
1046
|
+
const segments = path.split("/");
|
|
1047
|
+
for (const segment of segments) {
|
|
1048
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
1049
|
+
throw new StorageError(
|
|
1050
|
+
400,
|
|
1051
|
+
STORAGE_CODES.invalidPath,
|
|
1052
|
+
`\u5B58\u50A8\u8DEF\u5F84\u542B\u975E\u6CD5\u6BB5 "${segment}"\uFF08\u7981\u6B62\u7A7A\u6BB5 / . / .. \u7A7F\u8D8A\uFF09`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
if (/[\\]/.test(segment)) {
|
|
1056
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u5B58\u50A8\u8DEF\u5F84\u7981\u6B62\u53CD\u659C\u6760");
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return path;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// packages/runtime/src/storage/signature.ts
|
|
1063
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
1064
|
+
var DEFAULT_SIGN_TTL_SECONDS = 15 * 60;
|
|
1065
|
+
var SIGN_EXPIRES_KEY = "x-expires";
|
|
1066
|
+
var SIGN_SIGNATURE_KEY = "x-signature";
|
|
1067
|
+
function sign(secret, projectId, path, expires) {
|
|
1068
|
+
const payload = `${projectId}|${path}|${expires}`;
|
|
1069
|
+
return createHmac("sha256", secret).update(payload).digest("hex");
|
|
1070
|
+
}
|
|
1071
|
+
function signDownloadUrl(config, projectId, path, ttlSeconds = DEFAULT_SIGN_TTL_SECONDS) {
|
|
1072
|
+
const expires = Math.floor(Date.now() / 1e3) + ttlSeconds;
|
|
1073
|
+
const base = `${config.baseUrl}/api/v1/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}`;
|
|
1074
|
+
const signature = sign(config.secret, projectId, path, expires);
|
|
1075
|
+
return `${base}?${SIGN_EXPIRES_KEY}=${expires}&${SIGN_SIGNATURE_KEY}=${signature}`;
|
|
1076
|
+
}
|
|
1077
|
+
function verifyDownloadSignature(config, projectId, path, query, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
1078
|
+
const expiresRaw = query.expires;
|
|
1079
|
+
const signatureRaw = query.signature;
|
|
1080
|
+
if (typeof expiresRaw !== "string" || typeof signatureRaw !== "string") {
|
|
1081
|
+
return { ok: false, reason: "missing" };
|
|
1082
|
+
}
|
|
1083
|
+
if (!/^\d{1,15}$/.test(expiresRaw)) return { ok: false, reason: "malformed" };
|
|
1084
|
+
const expires = Number(expiresRaw);
|
|
1085
|
+
if (!Number.isFinite(expires)) return { ok: false, reason: "malformed" };
|
|
1086
|
+
if (nowSeconds > expires) return { ok: false, reason: "expired" };
|
|
1087
|
+
const expected = sign(config.secret, projectId, path, expires);
|
|
1088
|
+
const actual = Buffer.from(signatureRaw, "utf8");
|
|
1089
|
+
const candidate = Buffer.from(expected, "utf8");
|
|
1090
|
+
if (actual.length !== candidate.length || !timingSafeEqual(actual, candidate)) {
|
|
1091
|
+
return { ok: false, reason: "invalid" };
|
|
1092
|
+
}
|
|
1093
|
+
return { ok: true };
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// packages/runtime/src/storage/cloud.ts
|
|
1097
|
+
var DEFAULT_VISIBILITY = "private";
|
|
1098
|
+
function asBytes(data) {
|
|
1099
|
+
if (typeof data === "string") return new TextEncoder().encode(data);
|
|
1100
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
1101
|
+
if (data instanceof Uint8Array) return data;
|
|
1102
|
+
const candidate = data;
|
|
1103
|
+
if (candidate.data instanceof Uint8Array) return candidate.data;
|
|
1104
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u4E0D\u652F\u6301\u7684\u5B58\u50A8\u6570\u636E\u7C7B\u578B");
|
|
1105
|
+
}
|
|
1106
|
+
function toStoredFile(meta) {
|
|
1107
|
+
return {
|
|
1108
|
+
path: meta.path,
|
|
1109
|
+
size: meta.size,
|
|
1110
|
+
visibility: meta.visibility,
|
|
1111
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType },
|
|
1112
|
+
...meta.updatedAt === void 0 ? {} : { updatedAt: meta.updatedAt }
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function createStorageCapability(driver, signer, projectId) {
|
|
1116
|
+
const handler = async (method, args) => {
|
|
1117
|
+
switch (method) {
|
|
1118
|
+
case "upload": {
|
|
1119
|
+
const [path, data] = args;
|
|
1120
|
+
return toStoredFile(
|
|
1121
|
+
await driver.put(projectId, path, asBytes(data), { visibility: DEFAULT_VISIBILITY })
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
case "get": {
|
|
1125
|
+
const [path] = args;
|
|
1126
|
+
return (await driver.get(projectId, path)).data;
|
|
1127
|
+
}
|
|
1128
|
+
case "remove": {
|
|
1129
|
+
const [path] = args;
|
|
1130
|
+
await driver.remove(projectId, path);
|
|
1131
|
+
return void 0;
|
|
1132
|
+
}
|
|
1133
|
+
case "list": {
|
|
1134
|
+
const [prefix] = args;
|
|
1135
|
+
return (await driver.list(projectId, prefix)).map(toStoredFile);
|
|
1136
|
+
}
|
|
1137
|
+
case "getSignedUrl": {
|
|
1138
|
+
const [path, ttlSeconds] = args;
|
|
1139
|
+
return signDownloadUrl(signer, projectId, path, ttlSeconds);
|
|
1140
|
+
}
|
|
1141
|
+
default:
|
|
1142
|
+
throw new StorageError(
|
|
1143
|
+
400,
|
|
1144
|
+
"STORAGE_INVALID_METHOD",
|
|
1145
|
+
`\u672A\u77E5\u7684 cloud.storage \u65B9\u6CD5 "${method}"`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
return {
|
|
1150
|
+
capabilities: [{ name: "storage", value: { [RPC_CAPABILITY_KEY]: true } }],
|
|
1151
|
+
rpcHandlers: { storage: handler }
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// packages/runtime/src/storage/driver/local.ts
|
|
1156
|
+
import { mkdir, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
1157
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1158
|
+
var META_SUFFIX = ".adep-meta.json";
|
|
1159
|
+
function createLocalStorageDriver(options) {
|
|
1160
|
+
const quotaBytes = options.quotaBytes ?? PROJECT_QUOTA_BYTES;
|
|
1161
|
+
const bucketDir = (projectId) => join2(options.dir, projectId);
|
|
1162
|
+
const objectPath = (projectId, path) => join2(bucketDir(projectId), path);
|
|
1163
|
+
const metaPath = (projectId, path) => join2(dirname2(objectPath(projectId, path)), `${path.split("/").pop() ?? ""}${META_SUFFIX}`);
|
|
1164
|
+
const readStoredMeta = async (projectId, path) => {
|
|
1165
|
+
const raw = await readFile(metaPath(projectId, path), "utf8");
|
|
1166
|
+
return JSON.parse(raw);
|
|
1167
|
+
};
|
|
1168
|
+
const writeStoredMeta = async (projectId, path, meta) => {
|
|
1169
|
+
const payload = {
|
|
1170
|
+
visibility: meta.visibility,
|
|
1171
|
+
size: meta.size,
|
|
1172
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1173
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType }
|
|
1174
|
+
};
|
|
1175
|
+
await writeFile(metaPath(projectId, path), JSON.stringify(payload), "utf8");
|
|
1176
|
+
};
|
|
1177
|
+
const totalSize = async (projectId) => {
|
|
1178
|
+
const root = bucketDir(projectId);
|
|
1179
|
+
let total = 0;
|
|
1180
|
+
const walk = async (dir) => {
|
|
1181
|
+
let entries;
|
|
1182
|
+
try {
|
|
1183
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1184
|
+
} catch {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const tasks = [];
|
|
1188
|
+
for (const entry of entries) {
|
|
1189
|
+
const full = join2(dir, entry.name);
|
|
1190
|
+
if (entry.isDirectory()) {
|
|
1191
|
+
tasks.push(walk(full));
|
|
1192
|
+
} else if (entry.isFile() && !entry.name.endsWith(META_SUFFIX)) {
|
|
1193
|
+
tasks.push(
|
|
1194
|
+
(async () => {
|
|
1195
|
+
try {
|
|
1196
|
+
const info = await stat(full);
|
|
1197
|
+
total += info.size;
|
|
1198
|
+
} catch {
|
|
1199
|
+
}
|
|
1200
|
+
})()
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
await Promise.all(tasks);
|
|
1205
|
+
};
|
|
1206
|
+
await walk(root);
|
|
1207
|
+
return total;
|
|
1208
|
+
};
|
|
1209
|
+
return {
|
|
1210
|
+
async put(projectId, path, data, putOptions) {
|
|
1211
|
+
assertSafeStoragePath(path);
|
|
1212
|
+
if (!(data instanceof Uint8Array)) {
|
|
1213
|
+
throw new StorageError(400, "STORAGE_INVALID_PAYLOAD", "\u5199\u5165\u5185\u5BB9\u5FC5\u987B\u662F\u5B57\u8282\u6570\u7EC4");
|
|
1214
|
+
}
|
|
1215
|
+
const used = await totalSize(projectId);
|
|
1216
|
+
let existingSize = 0;
|
|
1217
|
+
try {
|
|
1218
|
+
existingSize = (await stat(objectPath(projectId, path))).size;
|
|
1219
|
+
} catch {
|
|
1220
|
+
existingSize = 0;
|
|
1221
|
+
}
|
|
1222
|
+
const projected = used - existingSize + data.byteLength;
|
|
1223
|
+
if (options.quotaCheck !== void 0) {
|
|
1224
|
+
await options.quotaCheck(projectId, projected);
|
|
1225
|
+
} else if (projected > quotaBytes) {
|
|
1226
|
+
throw new StorageError(
|
|
1227
|
+
413,
|
|
1228
|
+
STORAGE_CODES.quotaExceeded,
|
|
1229
|
+
`\u9879\u76EE\u5B58\u50A8\u7A7A\u95F4\u4E0D\u8DB3\uFF1A\u914D\u989D ${quotaBytes} \u5B57\u8282\u5DF2\u7528\u5C3D\uFF08STORAGE_QUOTA_EXCEEDED\uFF09`
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
const dest = objectPath(projectId, path);
|
|
1233
|
+
await mkdir(dirname2(dest), { recursive: true });
|
|
1234
|
+
await writeFile(dest, data);
|
|
1235
|
+
await writeStoredMeta(projectId, path, {
|
|
1236
|
+
visibility: putOptions.visibility,
|
|
1237
|
+
size: data.byteLength,
|
|
1238
|
+
...putOptions.contentType === void 0 ? {} : { contentType: putOptions.contentType }
|
|
1239
|
+
});
|
|
1240
|
+
const meta = await readStoredMeta(projectId, path);
|
|
1241
|
+
return {
|
|
1242
|
+
path,
|
|
1243
|
+
size: meta.size,
|
|
1244
|
+
visibility: meta.visibility,
|
|
1245
|
+
updatedAt: meta.updatedAt,
|
|
1246
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType }
|
|
1247
|
+
};
|
|
1248
|
+
},
|
|
1249
|
+
async get(projectId, path) {
|
|
1250
|
+
assertSafeStoragePath(path);
|
|
1251
|
+
let meta;
|
|
1252
|
+
try {
|
|
1253
|
+
meta = await readStoredMeta(projectId, path);
|
|
1254
|
+
} catch {
|
|
1255
|
+
throw new StorageError(404, STORAGE_CODES.notFound, `\u6587\u4EF6 "${path}" \u4E0D\u5B58\u5728`);
|
|
1256
|
+
}
|
|
1257
|
+
const data = await readFile(objectPath(projectId, path));
|
|
1258
|
+
const result = {
|
|
1259
|
+
path,
|
|
1260
|
+
size: meta.size,
|
|
1261
|
+
visibility: meta.visibility,
|
|
1262
|
+
updatedAt: meta.updatedAt,
|
|
1263
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType }
|
|
1264
|
+
};
|
|
1265
|
+
return { data, meta: result };
|
|
1266
|
+
},
|
|
1267
|
+
async getMeta(projectId, path) {
|
|
1268
|
+
assertSafeStoragePath(path);
|
|
1269
|
+
let meta;
|
|
1270
|
+
try {
|
|
1271
|
+
meta = await readStoredMeta(projectId, path);
|
|
1272
|
+
} catch {
|
|
1273
|
+
throw new StorageError(404, STORAGE_CODES.notFound, `\u6587\u4EF6 "${path}" \u4E0D\u5B58\u5728`);
|
|
1274
|
+
}
|
|
1275
|
+
const result = {
|
|
1276
|
+
path,
|
|
1277
|
+
size: meta.size,
|
|
1278
|
+
visibility: meta.visibility,
|
|
1279
|
+
updatedAt: meta.updatedAt,
|
|
1280
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType }
|
|
1281
|
+
};
|
|
1282
|
+
return result;
|
|
1283
|
+
},
|
|
1284
|
+
async remove(projectId, path) {
|
|
1285
|
+
assertSafeStoragePath(path);
|
|
1286
|
+
let exists = true;
|
|
1287
|
+
try {
|
|
1288
|
+
await stat(objectPath(projectId, path));
|
|
1289
|
+
} catch {
|
|
1290
|
+
exists = false;
|
|
1291
|
+
}
|
|
1292
|
+
if (!exists) {
|
|
1293
|
+
throw new StorageError(404, STORAGE_CODES.notFound, `\u6587\u4EF6 "${path}" \u4E0D\u5B58\u5728`);
|
|
1294
|
+
}
|
|
1295
|
+
await rm(objectPath(projectId, path), { force: true });
|
|
1296
|
+
await unlink(metaPath(projectId, path)).catch(() => void 0);
|
|
1297
|
+
},
|
|
1298
|
+
async list(projectId, prefix) {
|
|
1299
|
+
if (prefix !== void 0 && prefix !== "") {
|
|
1300
|
+
const segments = prefix.split("/");
|
|
1301
|
+
if (prefix.startsWith("/") || /[\\]/.test(prefix) || segments.some((segment) => segment === "..")) {
|
|
1302
|
+
throw new StorageError(
|
|
1303
|
+
400,
|
|
1304
|
+
STORAGE_CODES.invalidPath,
|
|
1305
|
+
`\u975E\u6CD5\u524D\u7F00 "${prefix}"\uFF08\u7981\u6B62 / \u5F00\u5934\u3001\u53CD\u659C\u6760\u6216 .. \u7A7F\u8D8A\uFF09`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
const root = bucketDir(projectId);
|
|
1310
|
+
const metas = [];
|
|
1311
|
+
const walk = async (dir, relative) => {
|
|
1312
|
+
let entries;
|
|
1313
|
+
try {
|
|
1314
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1315
|
+
} catch {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const tasks = [];
|
|
1319
|
+
for (const entry of entries) {
|
|
1320
|
+
if (entry.name.endsWith(META_SUFFIX)) continue;
|
|
1321
|
+
const full = join2(dir, entry.name);
|
|
1322
|
+
const rel = relative === "" ? entry.name : `${relative}/${entry.name}`;
|
|
1323
|
+
if (entry.isFile()) {
|
|
1324
|
+
if (prefix !== void 0 && !rel.startsWith(prefix)) continue;
|
|
1325
|
+
tasks.push(
|
|
1326
|
+
(async () => {
|
|
1327
|
+
try {
|
|
1328
|
+
const payload = JSON.parse(
|
|
1329
|
+
await readFile(full + META_SUFFIX, "utf8")
|
|
1330
|
+
);
|
|
1331
|
+
metas.push({
|
|
1332
|
+
path: rel,
|
|
1333
|
+
size: payload.size,
|
|
1334
|
+
visibility: payload.visibility,
|
|
1335
|
+
updatedAt: payload.updatedAt,
|
|
1336
|
+
...payload.contentType === void 0 ? {} : { contentType: payload.contentType }
|
|
1337
|
+
});
|
|
1338
|
+
} catch {
|
|
1339
|
+
}
|
|
1340
|
+
})()
|
|
1341
|
+
);
|
|
1342
|
+
} else if (entry.isDirectory()) {
|
|
1343
|
+
if (prefix !== void 0 && !rel.startsWith(prefix)) {
|
|
1344
|
+
const subIncluded = prefix.startsWith(rel + "/") || rel.startsWith(prefix);
|
|
1345
|
+
if (!subIncluded) continue;
|
|
1346
|
+
}
|
|
1347
|
+
tasks.push(walk(full, rel));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
await Promise.all(tasks);
|
|
1351
|
+
};
|
|
1352
|
+
await walk(root, "");
|
|
1353
|
+
metas.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1354
|
+
return metas;
|
|
1355
|
+
},
|
|
1356
|
+
async usage(projectId) {
|
|
1357
|
+
return totalSize(projectId);
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// packages/runtime/src/shared/function-source.ts
|
|
1363
|
+
async function resolveFunctionSource(db, fn) {
|
|
1364
|
+
const readDraft = async () => {
|
|
1365
|
+
const drafts = await db.functionFiles.listByFunction(fn.id);
|
|
1366
|
+
if (drafts.length === 0) return null;
|
|
1367
|
+
return Object.fromEntries(drafts.map((file) => [file.path, file.content]));
|
|
1368
|
+
};
|
|
1369
|
+
if (fn.publishedVersion === null || fn.publishedVersion === void 0) return readDraft();
|
|
1370
|
+
const version = await db.functionVersions.findByFunctionAndVersion(fn.id, fn.publishedVersion);
|
|
1371
|
+
if (version === null) return readDraft();
|
|
1372
|
+
return parseSnapshot(version.code);
|
|
1373
|
+
}
|
|
1374
|
+
function parseSnapshot(code) {
|
|
1375
|
+
return JSON.parse(code);
|
|
1376
|
+
}
|
|
1377
|
+
export {
|
|
1378
|
+
CHAIN_CAPABILITY_KEY,
|
|
1379
|
+
DB_RPC,
|
|
1380
|
+
DEFAULT_MEMORY_LIMIT_MB,
|
|
1381
|
+
DEFAULT_TIMEOUT_MS,
|
|
1382
|
+
ExecutorError,
|
|
1383
|
+
OOM_CODE,
|
|
1384
|
+
RPC_CAPABILITY_KEY,
|
|
1385
|
+
STORAGE_CODES,
|
|
1386
|
+
StorageError,
|
|
1387
|
+
TIMEOUT_CODE,
|
|
1388
|
+
WorkerFunctionExecutor,
|
|
1389
|
+
createCloudDb,
|
|
1390
|
+
createDbCapability,
|
|
1391
|
+
createLocalStorageDriver,
|
|
1392
|
+
createStorageCapability,
|
|
1393
|
+
parseSnapshot,
|
|
1394
|
+
resolveFunctionSource,
|
|
1395
|
+
signDownloadUrl,
|
|
1396
|
+
verifyDownloadSignature
|
|
1397
|
+
};
|