@adep/cli 0.0.9 → 0.1.1
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 +89 -38
- package/dist/index.js +2064 -1576
- package/dist/vite.js +2719 -0
- package/package.json +15 -5
package/dist/vite.js
ADDED
|
@@ -0,0 +1,2719 @@
|
|
|
1
|
+
// packages/cli/src/vite.ts
|
|
2
|
+
import { adepPlugin } from "@adep/vite-plugin";
|
|
3
|
+
|
|
4
|
+
// packages/cli/src/dev.ts
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { watch } from "node:fs";
|
|
7
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
8
|
+
import { basename, join as join7, resolve as resolve2 } from "node:path";
|
|
9
|
+
|
|
10
|
+
// packages/runtime/src/shared/capability-keys.ts
|
|
11
|
+
var RPC_CAPABILITY_KEY = "__adepRpc";
|
|
12
|
+
var CHAIN_CAPABILITY_KEY = "__adepChain";
|
|
13
|
+
var DB_RPC = {
|
|
14
|
+
/** 直通方法(如 query):`(method=query, args=[sql, params])`。 */
|
|
15
|
+
query: "query",
|
|
16
|
+
/** 读取 owned 表变更流(DB-006):`(method=changes, args=[table, query])`。 */
|
|
17
|
+
changes: "changes",
|
|
18
|
+
/** 开启事务:`begin → txId`。 */
|
|
19
|
+
begin: "begin",
|
|
20
|
+
/** 提交事务:`commit, args=[txId]`。 */
|
|
21
|
+
commit: "commit",
|
|
22
|
+
/** 回滚事务:`rollback, args=[txId]`。 */
|
|
23
|
+
rollback: "rollback",
|
|
24
|
+
/** 执行一条链:`chain, args=[ChainRequest]`。 */
|
|
25
|
+
chain: "chain"
|
|
26
|
+
};
|
|
27
|
+
var REALTIME_CAPABILITY_CODES = {
|
|
28
|
+
invalidMethod: "REALTIME_INVALID_METHOD",
|
|
29
|
+
unsupportedArg: "REALTIME_UNSUPPORTED_ARG",
|
|
30
|
+
subscriptionNotFound: "REALTIME_SUBSCRIPTION_NOT_FOUND",
|
|
31
|
+
tooManySubscriptions: "REALTIME_TOO_MANY_SUBSCRIPTIONS",
|
|
32
|
+
/** 线上独有:订阅授权策略(RT-002 `SubscriptionPolicy`)拒绝该 channel。 */
|
|
33
|
+
channelDenied: "REALTIME_CHANNEL_DENIED"
|
|
34
|
+
};
|
|
35
|
+
var REALTIME_CAPABILITY_METHODS = [
|
|
36
|
+
"publish",
|
|
37
|
+
"subscribe",
|
|
38
|
+
"receive",
|
|
39
|
+
"unsubscribe"
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// packages/runtime/src/shared/executor-runtime.ts
|
|
43
|
+
var ExecutorError = class extends Error {
|
|
44
|
+
constructor(status, code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.status = status;
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.name = "ExecutorError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var TIMEOUT_CODE = "FN_EXEC_TIMEOUT";
|
|
52
|
+
var OOM_CODE = "FN_EXEC_OOM";
|
|
53
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
54
|
+
var DEFAULT_MEMORY_LIMIT_MB = 128;
|
|
55
|
+
function isHttpStatus(status) {
|
|
56
|
+
return typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599;
|
|
57
|
+
}
|
|
58
|
+
function normalizeHttpStatus(status) {
|
|
59
|
+
return isHttpStatus(status) ? status : 500;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// packages/runtime/src/functions/runtime/worker-executor.ts
|
|
63
|
+
import { readFileSync } from "node:fs";
|
|
64
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
65
|
+
import { dirname, join } from "node:path";
|
|
66
|
+
import { Worker } from "node:worker_threads";
|
|
67
|
+
|
|
68
|
+
// packages/runtime/src/functions/deps/resolve.ts
|
|
69
|
+
import { resolve } from "node:path";
|
|
70
|
+
|
|
71
|
+
// packages/runtime/src/functions/deps/manifest.ts
|
|
72
|
+
import { createHash } from "node:crypto";
|
|
73
|
+
|
|
74
|
+
// packages/runtime/src/functions/domain.ts
|
|
75
|
+
var MAX_TOTAL_SOURCE_BYTES = 256 * 1024;
|
|
76
|
+
var FnError = class extends Error {
|
|
77
|
+
status;
|
|
78
|
+
code;
|
|
79
|
+
constructor(status, code, message) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.status = status;
|
|
82
|
+
this.code = code;
|
|
83
|
+
this.name = "FnError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// packages/runtime/src/functions/deps/manifest.ts
|
|
88
|
+
var PACKAGE_NAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-._~]+$/;
|
|
89
|
+
var COMPARATOR_PATTERN = /^(?:\^|~|>=|<=|>|<|=)?\d+(?:\.(?:\d+|x|X|\*)){0,2}(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
90
|
+
var MANIFEST_MAX_DEPS = 100;
|
|
91
|
+
function isVersionRangeSpec(spec) {
|
|
92
|
+
if (spec === "*" || spec === "latest") return true;
|
|
93
|
+
const unions = spec.split("||");
|
|
94
|
+
return unions.every((union) => {
|
|
95
|
+
const trimmed = union.trim();
|
|
96
|
+
if (trimmed.length === 0) return false;
|
|
97
|
+
const hyphen = trimmed.split(" - ");
|
|
98
|
+
if (hyphen.length === 2) {
|
|
99
|
+
return COMPARATOR_PATTERN.test(hyphen[0].trim()) && COMPARATOR_PATTERN.test(hyphen[1].trim());
|
|
100
|
+
}
|
|
101
|
+
return trimmed.split(/\s+/).every((part) => COMPARATOR_PATTERN.test(part));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function parseManifestDependencies(content) {
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(content);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new FnError(400, "DEP_INVALID_MANIFEST", "package.json \u4E0D\u662F\u5408\u6CD5 JSON");
|
|
110
|
+
}
|
|
111
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
112
|
+
throw new FnError(400, "DEP_INVALID_MANIFEST", "package.json \u9876\u5C42\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
113
|
+
}
|
|
114
|
+
const raw = parsed["dependencies"];
|
|
115
|
+
if (raw === void 0) return {};
|
|
116
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
117
|
+
throw new FnError(400, "DEP_INVALID_MANIFEST", "dependencies \u5FC5\u987B\u662F { \u5305\u540D: \u7248\u672C\u8303\u56F4 } \u5BF9\u8C61");
|
|
118
|
+
}
|
|
119
|
+
const entries = Object.entries(raw);
|
|
120
|
+
if (entries.length > MANIFEST_MAX_DEPS) {
|
|
121
|
+
throw new FnError(
|
|
122
|
+
400,
|
|
123
|
+
"DEP_INVALID_MANIFEST",
|
|
124
|
+
`\u4F9D\u8D56\u6761\u76EE\u8D85\u8FC7\u4E0A\u9650\uFF08${MANIFEST_MAX_DEPS} \u4E2A\uFF0C\u5F53\u524D ${entries.length} \u4E2A\uFF09`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const dependencies = {};
|
|
128
|
+
for (const [name, spec] of entries) {
|
|
129
|
+
if (!PACKAGE_NAME_PATTERN.test(name)) {
|
|
130
|
+
throw new FnError(
|
|
131
|
+
400,
|
|
132
|
+
"DEP_INVALID_MANIFEST",
|
|
133
|
+
`\u975E\u6CD5\u4F9D\u8D56\u540D "${name}"\uFF1A\u987B\u4E3A npm \u5305\u540D\uFF08scoped \u5F62\u5982 @scope/name\uFF09`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (typeof spec !== "string" || !isVersionRangeSpec(spec)) {
|
|
137
|
+
throw new FnError(
|
|
138
|
+
400,
|
|
139
|
+
"DEP_SPEC_REJECTED",
|
|
140
|
+
`\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`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
dependencies[name] = spec;
|
|
144
|
+
}
|
|
145
|
+
return dependencies;
|
|
146
|
+
}
|
|
147
|
+
function tryParseManifestDependencies(content) {
|
|
148
|
+
if (content === void 0) return {};
|
|
149
|
+
try {
|
|
150
|
+
return parseManifestDependencies(content);
|
|
151
|
+
} catch {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function classifyDependencies(dependencies, builtin) {
|
|
156
|
+
const builtinDeps = {};
|
|
157
|
+
const customDeps = {};
|
|
158
|
+
for (const [name, spec] of Object.entries(dependencies)) {
|
|
159
|
+
if (builtin.includes(name)) builtinDeps[name] = spec;
|
|
160
|
+
else customDeps[name] = spec;
|
|
161
|
+
}
|
|
162
|
+
return { builtinDeps, customDeps };
|
|
163
|
+
}
|
|
164
|
+
function depsKeyOf(dependencies) {
|
|
165
|
+
const canonical = Object.entries(dependencies).map(([name, version]) => `${name}@${version}`).toSorted().join("\n");
|
|
166
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// packages/runtime/src/functions/deps/resolve.ts
|
|
170
|
+
function resolveExecutionDeps(config, projectId, manifestContent) {
|
|
171
|
+
const manifest = tryParseManifestDependencies(manifestContent);
|
|
172
|
+
const { customDeps } = classifyDependencies(manifest, config.builtin);
|
|
173
|
+
const custom = Object.keys(customDeps);
|
|
174
|
+
return {
|
|
175
|
+
// 绝对化(相对进程 cwd,与 installer 侧 resolve(rootDir) 同基准):worker 的 createRequire 需要确定路径
|
|
176
|
+
dir: custom.length === 0 ? null : resolve(config.rootDir, projectId, depsKeyOf(customDeps)),
|
|
177
|
+
builtin: [...config.builtin],
|
|
178
|
+
custom
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// packages/runtime/src/functions/runtime/worker-executor.ts
|
|
183
|
+
function resolveWorkerEntry(baseDir = fileURLToPath(new URL(".", import.meta.url))) {
|
|
184
|
+
const candidates = [
|
|
185
|
+
join(baseDir, "worker-entry.js"),
|
|
186
|
+
join(baseDir, "worker-entry.ts"),
|
|
187
|
+
join(baseDir, "domains/functions/runtime/worker-entry.js"),
|
|
188
|
+
join(baseDir, "domains/functions/runtime/worker-entry.ts")
|
|
189
|
+
];
|
|
190
|
+
let current = baseDir;
|
|
191
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
192
|
+
candidates.push(
|
|
193
|
+
join(current, "packages/runtime/src/functions/runtime/worker-entry.js"),
|
|
194
|
+
join(current, "packages/runtime/src/functions/runtime/worker-entry.ts")
|
|
195
|
+
);
|
|
196
|
+
const parent = dirname(current);
|
|
197
|
+
if (parent === current) break;
|
|
198
|
+
current = parent;
|
|
199
|
+
}
|
|
200
|
+
for (const candidate of candidates) {
|
|
201
|
+
try {
|
|
202
|
+
readFileSync(candidate);
|
|
203
|
+
return pathToFileURL(candidate);
|
|
204
|
+
} catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D worker \u5165\u53E3\uFF08worker-entry.ts / worker-entry.js\uFF09");
|
|
209
|
+
}
|
|
210
|
+
function rpcReply(worker, id, ok, payload) {
|
|
211
|
+
worker.postMessage({
|
|
212
|
+
type: "rpc-response",
|
|
213
|
+
id,
|
|
214
|
+
ok,
|
|
215
|
+
...payload?.result === void 0 ? {} : { result: payload.result },
|
|
216
|
+
...payload?.error === void 0 ? {} : { error: payload.error }
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
var WorkerFunctionExecutor = class {
|
|
220
|
+
depsConfig;
|
|
221
|
+
fetchConfig;
|
|
222
|
+
constructor(options = {}) {
|
|
223
|
+
this.depsConfig = options.deps;
|
|
224
|
+
this.fetchConfig = options.fetch;
|
|
225
|
+
}
|
|
226
|
+
async execute(input) {
|
|
227
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
228
|
+
const memoryLimitMb = input.memoryLimitMb ?? DEFAULT_MEMORY_LIMIT_MB;
|
|
229
|
+
const entry = input.entry ?? "index.ts";
|
|
230
|
+
const deps = this.depsConfig === void 0 ? void 0 : resolveExecutionDeps(this.depsConfig, input.project.id, input.files["package.json"]);
|
|
231
|
+
const logs = [];
|
|
232
|
+
return new Promise((resolve3, reject) => {
|
|
233
|
+
let worker;
|
|
234
|
+
try {
|
|
235
|
+
worker = new Worker(resolveWorkerEntry(), {
|
|
236
|
+
workerData: {
|
|
237
|
+
files: input.files,
|
|
238
|
+
entry,
|
|
239
|
+
request: input.request,
|
|
240
|
+
capabilities: input.capabilities ?? [],
|
|
241
|
+
...input.env === void 0 ? {} : { env: input.env },
|
|
242
|
+
...deps === void 0 ? {} : { deps },
|
|
243
|
+
...this.fetchConfig === void 0 ? {} : { fetch: this.fetchConfig }
|
|
244
|
+
},
|
|
245
|
+
resourceLimits: { maxOldGenerationSizeMb: memoryLimitMb }
|
|
246
|
+
});
|
|
247
|
+
} catch (error) {
|
|
248
|
+
reject(
|
|
249
|
+
new ExecutorError(
|
|
250
|
+
500,
|
|
251
|
+
"FN_EXEC_ERROR",
|
|
252
|
+
error instanceof Error ? error.message : "worker \u6784\u9020\u5931\u8D25"
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const timer = setTimeout(() => {
|
|
258
|
+
void worker.terminate();
|
|
259
|
+
reject(
|
|
260
|
+
new ExecutorError(504, TIMEOUT_CODE, `\u51FD\u6570\u6267\u884C\u8D85\u8FC7 ${timeoutMs}ms \u9650\u5236\uFF0Cworker \u5DF2\u88AB\u56DE\u6536`)
|
|
261
|
+
);
|
|
262
|
+
}, timeoutMs);
|
|
263
|
+
worker.on(
|
|
264
|
+
"message",
|
|
265
|
+
(message) => {
|
|
266
|
+
if (message.type === "rpc") {
|
|
267
|
+
const handler = input.rpcHandlers?.[message.capability ?? ""];
|
|
268
|
+
if (handler === void 0) {
|
|
269
|
+
rpcReply(worker, message.id, false, {
|
|
270
|
+
error: `RPC \u80FD\u529B "${message.capability ?? ""}" \u672A\u6CE8\u518C`
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const rpcArgs = Array.isArray(message.args) ? message.args : [];
|
|
275
|
+
void (async () => {
|
|
276
|
+
try {
|
|
277
|
+
const result = await handler(message.method ?? "", rpcArgs);
|
|
278
|
+
rpcReply(worker, message.id, true, { result });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
rpcReply(worker, message.id, false, {
|
|
281
|
+
error: error instanceof Error ? error.message : String(error)
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
})();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (message.type === "log") {
|
|
288
|
+
logs.push(`[${message.level ?? "log"}] ${message.message ?? ""}`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (message.type === "result") {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
void worker.terminate();
|
|
294
|
+
resolve3({ body: message.body, logs });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (message.type === "error") {
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
void worker.terminate();
|
|
300
|
+
const isOom = typeof message.message === "string" && /allocation\s+(failed|error)|out\s+of\s+memory|\bOOM\b/i.test(message.message);
|
|
301
|
+
reject(
|
|
302
|
+
isOom ? new ExecutorError(500, OOM_CODE, "\u51FD\u6570\u5185\u5B58\u8D85\u9650\uFF0C\u6267\u884C\u5DF2\u4E2D\u6B62") : (
|
|
303
|
+
// FN-011:函数抛错携带的 status 透传落 ExecutorError.status(非法/缺失 → 500 兼容旧形态)。
|
|
304
|
+
new ExecutorError(
|
|
305
|
+
normalizeHttpStatus(message.status),
|
|
306
|
+
message.code ?? "FN_EXEC_ERROR",
|
|
307
|
+
message.message ?? "\u51FD\u6570\u6267\u884C\u5931\u8D25"
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
worker.on("error", (error) => {
|
|
315
|
+
clearTimeout(timer);
|
|
316
|
+
const isOom = /allocation\s+(failed|error)|out\s+of\s+memory|\bOOM\b/i.test(error.message);
|
|
317
|
+
reject(
|
|
318
|
+
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)
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async dispose() {
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// packages/cli/src/sim/runtime.ts
|
|
328
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
329
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
330
|
+
|
|
331
|
+
// packages/runtime/src/database/builder/dialect.ts
|
|
332
|
+
var sqliteDialect = {
|
|
333
|
+
name: "sqlite",
|
|
334
|
+
placeholder: () => "?",
|
|
335
|
+
quote: (identifier) => `"${identifier.replace(/"/g, '""')}"`,
|
|
336
|
+
lastInsertIdClause: () => "",
|
|
337
|
+
upsertConflictClause: (columns) => ` ON CONFLICT (${columns.map((column) => `"${column.replace(/"/g, '""')}"`).join(", ")}) DO NOTHING`
|
|
338
|
+
};
|
|
339
|
+
var postgresDialect = {
|
|
340
|
+
name: "postgres",
|
|
341
|
+
placeholder: (index) => `$${index + 1}`,
|
|
342
|
+
quote: (identifier) => `"${identifier.replace(/"/g, '""')}"`,
|
|
343
|
+
lastInsertIdClause: () => " RETURNING id",
|
|
344
|
+
upsertConflictClause: (columns) => ` ON CONFLICT (${columns.map((column) => `"${column.replace(/"/g, '""')}"`).join(", ")}) DO NOTHING`
|
|
345
|
+
};
|
|
346
|
+
function dialectFor(driver) {
|
|
347
|
+
return driver.engine === "pg" ? postgresDialect : sqliteDialect;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// packages/runtime/src/database/provision/identifier.ts
|
|
351
|
+
var DbError = class extends Error {
|
|
352
|
+
constructor(status, code, message) {
|
|
353
|
+
super(message);
|
|
354
|
+
this.status = status;
|
|
355
|
+
this.code = code;
|
|
356
|
+
this.name = "DbError";
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
360
|
+
function assertIdentifier(name) {
|
|
361
|
+
if (!IDENTIFIER_PATTERN.test(name)) {
|
|
362
|
+
throw new DbError(
|
|
363
|
+
400,
|
|
364
|
+
"DB_UNSAFE_OP",
|
|
365
|
+
`\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`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function quoteIdentifier(name) {
|
|
370
|
+
assertIdentifier(name);
|
|
371
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// packages/runtime/src/database/builder/guards.ts
|
|
375
|
+
function unsafeOperation(message) {
|
|
376
|
+
return new DbError(400, "DB_UNSAFE_OP", message);
|
|
377
|
+
}
|
|
378
|
+
function stripLiterals(sql) {
|
|
379
|
+
return sql.replace(/'[^']*(?:''[^']*)*'/g, "").replace(/"[^"]*(?:""[^"]*)*"/g, "").replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
380
|
+
}
|
|
381
|
+
function assertReadOnlyQuery(sql) {
|
|
382
|
+
const stripped = stripLiterals(sql);
|
|
383
|
+
if (stripped.includes(";")) {
|
|
384
|
+
throw unsafeOperation("\u4EC5\u5141\u8BB8\u5355\u6761\u8BED\u53E5\uFF0C\u7981\u6B62\u591A\u8BED\u53E5\u5806\u53E0");
|
|
385
|
+
}
|
|
386
|
+
const head = stripped.trim().replace(/^\(+/, "").trim().toUpperCase();
|
|
387
|
+
if (!head.startsWith("SELECT") && !head.startsWith("WITH")) {
|
|
388
|
+
throw unsafeOperation("cloud.db.query \u4EC5\u5141\u8BB8\u53EA\u8BFB\u67E5\u8BE2\uFF08SELECT / WITH\uFF09");
|
|
389
|
+
}
|
|
390
|
+
if (/ATTACH|DETACH/.test(stripped.toUpperCase())) {
|
|
391
|
+
throw unsafeOperation("\u7981\u6B62\u8DE8\u5E93\u64CD\u4F5C\uFF08ATTACH / DETACH\uFF09");
|
|
392
|
+
}
|
|
393
|
+
if (/SQLITE_\w+/i.test(stripped)) {
|
|
394
|
+
throw unsafeOperation("\u7981\u6B62\u8BBF\u95EE\u7CFB\u7EDF\u8868\uFF08sqlite_*\uFF09");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// packages/runtime/src/database/builder/owned.ts
|
|
399
|
+
var OWNED_PK = "id";
|
|
400
|
+
var OWNED_OWNER_KEY = "__owner_key";
|
|
401
|
+
var CHANGE_LOG_PREFIX = "_adep_changes_";
|
|
402
|
+
function changeLogTable(table) {
|
|
403
|
+
const name = `${CHANGE_LOG_PREFIX}${table}`;
|
|
404
|
+
assertIdentifier(name);
|
|
405
|
+
return name;
|
|
406
|
+
}
|
|
407
|
+
function assertUserColumn(name) {
|
|
408
|
+
assertIdentifier(name);
|
|
409
|
+
if (name.startsWith("__") && name !== OWNED_OWNER_KEY) {
|
|
410
|
+
throw new DbError(400, "DB_UNSAFE_OP", `\u4FDD\u7559\u524D\u7F00 "__" \u7684\u5217\u540D\u4E0D\u88AB\u5141\u8BB8\uFF1A${name.slice(0, 32)}`);
|
|
411
|
+
}
|
|
412
|
+
if (name.startsWith("_adep_")) {
|
|
413
|
+
throw new DbError(
|
|
414
|
+
400,
|
|
415
|
+
"DB_UNSAFE_OP",
|
|
416
|
+
`\u5E73\u53F0\u4FDD\u7559\u6BB5 "_adep_" \u7684\u5217\u540D\u4E0D\u88AB\u5141\u8BB8\uFF1A${name.slice(0, 32)}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function hasChangeLog(driver, table) {
|
|
421
|
+
if (driver.engine === "pg") {
|
|
422
|
+
return await driver.get(
|
|
423
|
+
"SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1",
|
|
424
|
+
[changeLogTable(table)]
|
|
425
|
+
) !== null;
|
|
426
|
+
}
|
|
427
|
+
return await driver.get("SELECT 1 AS x FROM sqlite_master WHERE type = ? AND name = ?", [
|
|
428
|
+
"table",
|
|
429
|
+
changeLogTable(table)
|
|
430
|
+
]) !== null;
|
|
431
|
+
}
|
|
432
|
+
async function assertOwned(driver, table) {
|
|
433
|
+
if (!await hasChangeLog(driver, table)) {
|
|
434
|
+
throw unsafeOperation(`\u8868 "${table}" \u4E0D\u662F owned \u8868\uFF08\u7F3A\u5C11\u53D8\u66F4\u6D41\u8868 ${changeLogTable(table)}\uFF09`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function appendChange(driver, table, op, id, ownerKey, before, after) {
|
|
438
|
+
const ph = dialectFor(driver).placeholder;
|
|
439
|
+
await driver.run(
|
|
440
|
+
`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)})`,
|
|
441
|
+
[
|
|
442
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
443
|
+
op,
|
|
444
|
+
id,
|
|
445
|
+
ownerKey,
|
|
446
|
+
before === null ? null : JSON.stringify(before),
|
|
447
|
+
after === null ? null : JSON.stringify(after)
|
|
448
|
+
]
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
async function runOwnedWrite(driver, fn) {
|
|
452
|
+
await driver.run("SAVEPOINT _adep_change");
|
|
453
|
+
try {
|
|
454
|
+
await fn(driver);
|
|
455
|
+
await driver.run("RELEASE SAVEPOINT _adep_change");
|
|
456
|
+
} catch (error) {
|
|
457
|
+
await driver.run("ROLLBACK TO SAVEPOINT _adep_change");
|
|
458
|
+
await driver.run("RELEASE SAVEPOINT _adep_change");
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function parseChange(row) {
|
|
463
|
+
const before = row["before"];
|
|
464
|
+
const after = row["after"];
|
|
465
|
+
return {
|
|
466
|
+
seq: Number(row["seq"]),
|
|
467
|
+
ts: String(row["ts"]),
|
|
468
|
+
op: row["op"],
|
|
469
|
+
id: String(row["id"]),
|
|
470
|
+
ownerKey: row["__owner_key"] === null || row["__owner_key"] === void 0 ? null : String(row["__owner_key"]),
|
|
471
|
+
before: before === null || before === void 0 ? null : JSON.parse(String(before)),
|
|
472
|
+
after: after === null || after === void 0 ? null : JSON.parse(String(after))
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async function readChanges(driver, table, query = {}) {
|
|
476
|
+
await assertOwned(driver, table);
|
|
477
|
+
const ph = dialectFor(driver).placeholder;
|
|
478
|
+
const params = [];
|
|
479
|
+
const conditions = [];
|
|
480
|
+
if (query.afterSeq !== void 0) {
|
|
481
|
+
if (!Number.isInteger(query.afterSeq) || query.afterSeq < 0) {
|
|
482
|
+
throw unsafeOperation("afterSeq \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
483
|
+
}
|
|
484
|
+
conditions.push(`"seq" > ${ph(params.length)}`);
|
|
485
|
+
params.push(query.afterSeq);
|
|
486
|
+
}
|
|
487
|
+
if (query.ownerKey !== void 0) {
|
|
488
|
+
if (query.ownerKey === null) {
|
|
489
|
+
conditions.push('"__owner_key" IS NULL');
|
|
490
|
+
} else {
|
|
491
|
+
conditions.push(`"__owner_key" = ${ph(params.length)}`);
|
|
492
|
+
params.push(query.ownerKey);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (query.limit !== void 0) {
|
|
496
|
+
if (!Number.isInteger(query.limit) || query.limit < 0) {
|
|
497
|
+
throw unsafeOperation("limit \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
let sql = `SELECT "seq","ts","op","id","__owner_key","before","after" FROM ` + quoteIdentifier(changeLogTable(table));
|
|
501
|
+
if (conditions.length > 0) sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
502
|
+
sql += ' ORDER BY "seq" ASC';
|
|
503
|
+
if (query.limit !== void 0) {
|
|
504
|
+
sql += ` LIMIT ${ph(params.length)}`;
|
|
505
|
+
params.push(query.limit);
|
|
506
|
+
}
|
|
507
|
+
const rows = await driver.all(sql, params);
|
|
508
|
+
return rows.map(parseChange);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// packages/runtime/src/database/builder/ulid.ts
|
|
512
|
+
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
513
|
+
var TIME_LEN = 10;
|
|
514
|
+
var RANDOM_LEN = 16;
|
|
515
|
+
var lastTime = 0;
|
|
516
|
+
var lastRandom = "";
|
|
517
|
+
function encodeTime(now) {
|
|
518
|
+
let ts = Math.trunc(now);
|
|
519
|
+
let out = "";
|
|
520
|
+
for (let i = 0; i < TIME_LEN; i++) {
|
|
521
|
+
out = ENCODING[ts % 32] + out;
|
|
522
|
+
ts = Math.floor(ts / 32);
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
function encodeRandom(bytes) {
|
|
527
|
+
let out = "";
|
|
528
|
+
let buffer = 0;
|
|
529
|
+
let bits = 0;
|
|
530
|
+
for (const byte of bytes) {
|
|
531
|
+
buffer = buffer << 8 | byte;
|
|
532
|
+
bits += 8;
|
|
533
|
+
while (bits >= 5) {
|
|
534
|
+
out += ENCODING[buffer >>> bits - 5 & 31];
|
|
535
|
+
bits -= 5;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return out.slice(0, RANDOM_LEN);
|
|
539
|
+
}
|
|
540
|
+
function incrBase32(prev) {
|
|
541
|
+
const chars = prev.split("");
|
|
542
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
543
|
+
const idx = ENCODING.indexOf(chars[i]);
|
|
544
|
+
if (idx < 31) {
|
|
545
|
+
chars[i] = ENCODING[idx + 1];
|
|
546
|
+
return chars.join("");
|
|
547
|
+
}
|
|
548
|
+
chars[i] = ENCODING[0];
|
|
549
|
+
}
|
|
550
|
+
return chars.join("");
|
|
551
|
+
}
|
|
552
|
+
function randomFill(bytes) {
|
|
553
|
+
const source = globalThis.crypto;
|
|
554
|
+
if (source !== void 0 && typeof source.getRandomValues === "function") {
|
|
555
|
+
return source.getRandomValues(bytes);
|
|
556
|
+
}
|
|
557
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
558
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
559
|
+
}
|
|
560
|
+
return bytes;
|
|
561
|
+
}
|
|
562
|
+
function ulid(now = Date.now()) {
|
|
563
|
+
const time = encodeTime(now);
|
|
564
|
+
let random;
|
|
565
|
+
if (now === lastTime) {
|
|
566
|
+
random = incrBase32(lastRandom);
|
|
567
|
+
} else {
|
|
568
|
+
lastTime = now;
|
|
569
|
+
const bytes = new Uint8Array(10);
|
|
570
|
+
randomFill(bytes);
|
|
571
|
+
random = encodeRandom(bytes);
|
|
572
|
+
}
|
|
573
|
+
lastRandom = random;
|
|
574
|
+
return time + random;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// packages/runtime/src/database/builder/table.ts
|
|
578
|
+
var OPERATORS = /* @__PURE__ */ new Set([
|
|
579
|
+
"=",
|
|
580
|
+
"!=",
|
|
581
|
+
">",
|
|
582
|
+
">=",
|
|
583
|
+
"<",
|
|
584
|
+
"<=",
|
|
585
|
+
"like",
|
|
586
|
+
"in",
|
|
587
|
+
"not in"
|
|
588
|
+
]);
|
|
589
|
+
var LIST_OPERATORS = /* @__PURE__ */ new Set(["in", "not in"]);
|
|
590
|
+
function pushParam(params, dialect, value) {
|
|
591
|
+
params.push(value);
|
|
592
|
+
return dialect.placeholder(params.length - 1);
|
|
593
|
+
}
|
|
594
|
+
function assertHasWhere(state) {
|
|
595
|
+
if (state.wheres.length === 0) {
|
|
596
|
+
throw unsafeOperation("update / delete \u5FC5\u987B\u5148\u6307\u5B9A where\uFF08\u9632\u6B62\u5168\u8868\u8BEF\u6539 / \u8BEF\u5220\uFF09");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async function resolveOwned(state, driver) {
|
|
600
|
+
if (state.owned === true) {
|
|
601
|
+
await assertOwned(driver, state.table);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
return state.owned ?? await hasChangeLog(driver, state.table);
|
|
605
|
+
}
|
|
606
|
+
function toOwnerKey(row) {
|
|
607
|
+
const value = row[OWNED_OWNER_KEY];
|
|
608
|
+
return value === null || value === void 0 ? null : String(value);
|
|
609
|
+
}
|
|
610
|
+
function ownedInsertRow(row) {
|
|
611
|
+
const effective = { ...row };
|
|
612
|
+
if (effective[OWNED_PK] === void 0) {
|
|
613
|
+
effective[OWNED_PK] = ulid();
|
|
614
|
+
}
|
|
615
|
+
return effective;
|
|
616
|
+
}
|
|
617
|
+
async function captureBeforeRows(state, dialect, driver) {
|
|
618
|
+
const select = compileSelect({ ...state, columns: [] }, dialect);
|
|
619
|
+
return driver.all(select.sql, select.params);
|
|
620
|
+
}
|
|
621
|
+
function compileWhere(wheres, dialect, params) {
|
|
622
|
+
return wheres.map((where) => {
|
|
623
|
+
const quoted = dialect.quote(where.column);
|
|
624
|
+
if (LIST_OPERATORS.has(where.operator)) {
|
|
625
|
+
const values = where.value;
|
|
626
|
+
if (values.length === 0) {
|
|
627
|
+
throw unsafeOperation(`${where.operator.toUpperCase()} \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u503C`);
|
|
628
|
+
}
|
|
629
|
+
const marks = values.map((value2) => pushParam(params, dialect, value2));
|
|
630
|
+
return `${quoted} ${where.operator.toUpperCase()} (${marks.join(", ")})`;
|
|
631
|
+
}
|
|
632
|
+
const value = where.value;
|
|
633
|
+
if (value === null) {
|
|
634
|
+
if (where.operator === "=") return `${quoted} IS NULL`;
|
|
635
|
+
if (where.operator === "!=") return `${quoted} IS NOT NULL`;
|
|
636
|
+
}
|
|
637
|
+
const mark = pushParam(params, dialect, value);
|
|
638
|
+
return `${quoted} ${where.operator} ${mark}`;
|
|
639
|
+
}).join(" AND ");
|
|
640
|
+
}
|
|
641
|
+
function compileSelect(state, dialect) {
|
|
642
|
+
const params = [];
|
|
643
|
+
const columns = state.columns.length > 0 ? state.columns.map((column) => dialect.quote(column)).join(", ") : "*";
|
|
644
|
+
let sql = `SELECT ${columns} FROM ${dialect.quote(state.table)}`;
|
|
645
|
+
if (state.wheres.length > 0) {
|
|
646
|
+
sql += ` WHERE ${compileWhere(state.wheres, dialect, params)}`;
|
|
647
|
+
}
|
|
648
|
+
if (state.orderBys.length > 0) {
|
|
649
|
+
sql += ` ORDER BY ${state.orderBys.map((order) => `${dialect.quote(order.column)} ${order.direction.toUpperCase()}`).join(", ")}`;
|
|
650
|
+
}
|
|
651
|
+
if (state.limit !== void 0) sql += ` LIMIT ${pushParam(params, dialect, state.limit)}`;
|
|
652
|
+
if (state.offset !== void 0) sql += ` OFFSET ${pushParam(params, dialect, state.offset)}`;
|
|
653
|
+
return { sql, params };
|
|
654
|
+
}
|
|
655
|
+
function compileCount(state, dialect) {
|
|
656
|
+
const params = [];
|
|
657
|
+
let sql = `SELECT count(*) AS n FROM ${dialect.quote(state.table)}`;
|
|
658
|
+
if (state.wheres.length > 0) {
|
|
659
|
+
sql += ` WHERE ${compileWhere(state.wheres, dialect, params)}`;
|
|
660
|
+
}
|
|
661
|
+
return { sql, params };
|
|
662
|
+
}
|
|
663
|
+
function compileInsert(table, row, dialect) {
|
|
664
|
+
const entries = Object.entries(row);
|
|
665
|
+
if (entries.length === 0) {
|
|
666
|
+
throw unsafeOperation("insert \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u5B57\u6BB5");
|
|
667
|
+
}
|
|
668
|
+
const params = [];
|
|
669
|
+
const columns = entries.map(([name]) => {
|
|
670
|
+
assertUserColumn(name);
|
|
671
|
+
return dialect.quote(name);
|
|
672
|
+
}).join(", ");
|
|
673
|
+
const marks = entries.map(([, value]) => pushParam(params, dialect, value));
|
|
674
|
+
return {
|
|
675
|
+
sql: `INSERT INTO ${dialect.quote(table)} (${columns}) VALUES (${marks.join(", ")})`,
|
|
676
|
+
params
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function compileInsertMany(table, rows, dialect) {
|
|
680
|
+
if (rows.length === 0) {
|
|
681
|
+
throw unsafeOperation("insertMany \u81F3\u5C11\u9700\u8981\u4E00\u884C");
|
|
682
|
+
}
|
|
683
|
+
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
684
|
+
if (columns.length === 0) {
|
|
685
|
+
throw unsafeOperation("insertMany \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u5B57\u6BB5");
|
|
686
|
+
}
|
|
687
|
+
columns.forEach(assertUserColumn);
|
|
688
|
+
const params = [];
|
|
689
|
+
const quoted = columns.map((column) => dialect.quote(column)).join(", ");
|
|
690
|
+
const valueGroups = rows.map((row) => {
|
|
691
|
+
const marks = columns.map((column) => pushParam(params, dialect, row[column] ?? null));
|
|
692
|
+
return `(${marks.join(", ")})`;
|
|
693
|
+
});
|
|
694
|
+
return {
|
|
695
|
+
sql: `INSERT INTO ${dialect.quote(table)} (${quoted}) VALUES ${valueGroups.join(", ")}`,
|
|
696
|
+
params
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function compileUpdate(table, wheres, values, dialect) {
|
|
700
|
+
const entries = Object.entries(values);
|
|
701
|
+
if (entries.length === 0) {
|
|
702
|
+
throw unsafeOperation("update \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u5B57\u6BB5");
|
|
703
|
+
}
|
|
704
|
+
const params = [];
|
|
705
|
+
const sets = entries.map(([name, value]) => {
|
|
706
|
+
assertUserColumn(name);
|
|
707
|
+
return `${dialect.quote(name)} = ${pushParam(params, dialect, value)}`;
|
|
708
|
+
}).join(", ");
|
|
709
|
+
const where = compileWhere(wheres, dialect, params);
|
|
710
|
+
return { sql: `UPDATE ${dialect.quote(table)} SET ${sets} WHERE ${where}`, params };
|
|
711
|
+
}
|
|
712
|
+
function compileDelete(table, wheres, dialect) {
|
|
713
|
+
const params = [];
|
|
714
|
+
const where = compileWhere(wheres, dialect, params);
|
|
715
|
+
return { sql: `DELETE FROM ${dialect.quote(table)} WHERE ${where}`, params };
|
|
716
|
+
}
|
|
717
|
+
function createTableBuilder(table, driver, dialect, options = {}) {
|
|
718
|
+
assertIdentifier(table);
|
|
719
|
+
const guardWrite = options.guardWrite ?? (() => void 0);
|
|
720
|
+
const initialState = {
|
|
721
|
+
table,
|
|
722
|
+
columns: [],
|
|
723
|
+
wheres: [],
|
|
724
|
+
orderBys: [],
|
|
725
|
+
limit: void 0,
|
|
726
|
+
offset: void 0
|
|
727
|
+
};
|
|
728
|
+
const build = (state) => {
|
|
729
|
+
const derived = (patch) => build({ ...state, ...patch });
|
|
730
|
+
return {
|
|
731
|
+
select(...columns) {
|
|
732
|
+
columns.forEach(assertUserColumn);
|
|
733
|
+
return derived({ columns });
|
|
734
|
+
},
|
|
735
|
+
owned() {
|
|
736
|
+
return derived({ owned: true });
|
|
737
|
+
},
|
|
738
|
+
where(column, operatorOrValue, maybeValue) {
|
|
739
|
+
assertUserColumn(column);
|
|
740
|
+
let operator;
|
|
741
|
+
let value;
|
|
742
|
+
if (maybeValue !== void 0) {
|
|
743
|
+
operator = operatorOrValue;
|
|
744
|
+
if (!OPERATORS.has(operator)) {
|
|
745
|
+
throw unsafeOperation(`\u4E0D\u652F\u6301\u7684\u64CD\u4F5C\u7B26 "${operator}"`);
|
|
746
|
+
}
|
|
747
|
+
if (LIST_OPERATORS.has(operator) && !Array.isArray(maybeValue)) {
|
|
748
|
+
throw unsafeOperation(`${operator.toUpperCase()} \u9700\u8981\u6570\u7EC4\u503C`);
|
|
749
|
+
}
|
|
750
|
+
value = maybeValue;
|
|
751
|
+
} else {
|
|
752
|
+
operator = "=";
|
|
753
|
+
value = operatorOrValue;
|
|
754
|
+
}
|
|
755
|
+
return derived({ wheres: [...state.wheres, { column, operator, value }] });
|
|
756
|
+
},
|
|
757
|
+
orderBy(column, direction = "asc") {
|
|
758
|
+
assertUserColumn(column);
|
|
759
|
+
if (direction !== "asc" && direction !== "desc") {
|
|
760
|
+
throw unsafeOperation("orderBy \u65B9\u5411\u4EC5\u652F\u6301 asc / desc");
|
|
761
|
+
}
|
|
762
|
+
return derived({ orderBys: [...state.orderBys, { column, direction }] });
|
|
763
|
+
},
|
|
764
|
+
limit(n) {
|
|
765
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
766
|
+
throw unsafeOperation("limit \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
767
|
+
}
|
|
768
|
+
return derived({ limit: n });
|
|
769
|
+
},
|
|
770
|
+
offset(n) {
|
|
771
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
772
|
+
throw unsafeOperation("offset \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
773
|
+
}
|
|
774
|
+
return derived({ offset: n });
|
|
775
|
+
},
|
|
776
|
+
async get() {
|
|
777
|
+
const compiled = compileSelect(state, dialect);
|
|
778
|
+
return driver.all(compiled.sql, compiled.params);
|
|
779
|
+
},
|
|
780
|
+
async first() {
|
|
781
|
+
const compiled = compileSelect({ ...state, limit: 1 }, dialect);
|
|
782
|
+
return driver.get(compiled.sql, compiled.params);
|
|
783
|
+
},
|
|
784
|
+
async count() {
|
|
785
|
+
const compiled = compileCount(state, dialect);
|
|
786
|
+
const row = await driver.get(compiled.sql, compiled.params);
|
|
787
|
+
return row === null ? 0 : Number(row["n"]);
|
|
788
|
+
},
|
|
789
|
+
async insert(row) {
|
|
790
|
+
guardWrite();
|
|
791
|
+
Object.keys(row).forEach(assertUserColumn);
|
|
792
|
+
if (await resolveOwned(state, driver)) {
|
|
793
|
+
const effective = ownedInsertRow(row);
|
|
794
|
+
const ownerKey = toOwnerKey(effective);
|
|
795
|
+
const compiled2 = compileInsert(state.table, effective, dialect);
|
|
796
|
+
await runOwnedWrite(driver, async (d) => {
|
|
797
|
+
await d.run(compiled2.sql, compiled2.params);
|
|
798
|
+
await appendChange(
|
|
799
|
+
d,
|
|
800
|
+
state.table,
|
|
801
|
+
"insert",
|
|
802
|
+
String(effective[OWNED_PK]),
|
|
803
|
+
ownerKey,
|
|
804
|
+
null,
|
|
805
|
+
effective
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const compiled = compileInsert(state.table, row, dialect);
|
|
811
|
+
await driver.run(compiled.sql, compiled.params);
|
|
812
|
+
},
|
|
813
|
+
async insertMany(rows) {
|
|
814
|
+
guardWrite();
|
|
815
|
+
const insertColumns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
816
|
+
insertColumns.forEach(assertUserColumn);
|
|
817
|
+
if (await resolveOwned(state, driver)) {
|
|
818
|
+
const effectiveRows = rows.map(ownedInsertRow);
|
|
819
|
+
const compiled2 = compileInsertMany(state.table, effectiveRows, dialect);
|
|
820
|
+
await runOwnedWrite(driver, async (d) => {
|
|
821
|
+
await d.run(compiled2.sql, compiled2.params);
|
|
822
|
+
for (const effective of effectiveRows) {
|
|
823
|
+
await appendChange(
|
|
824
|
+
d,
|
|
825
|
+
state.table,
|
|
826
|
+
"insert",
|
|
827
|
+
String(effective[OWNED_PK]),
|
|
828
|
+
toOwnerKey(effective),
|
|
829
|
+
null,
|
|
830
|
+
effective
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const compiled = compileInsertMany(state.table, rows, dialect);
|
|
837
|
+
await driver.run(compiled.sql, compiled.params);
|
|
838
|
+
},
|
|
839
|
+
async update(values) {
|
|
840
|
+
guardWrite();
|
|
841
|
+
assertHasWhere(state);
|
|
842
|
+
Object.keys(values).forEach(assertUserColumn);
|
|
843
|
+
if (await resolveOwned(state, driver)) {
|
|
844
|
+
const before = await captureBeforeRows(state, dialect, driver);
|
|
845
|
+
const compiled2 = compileUpdate(state.table, state.wheres, values, dialect);
|
|
846
|
+
await runOwnedWrite(driver, async (d) => {
|
|
847
|
+
await d.run(compiled2.sql, compiled2.params);
|
|
848
|
+
for (const row of before) {
|
|
849
|
+
const after = { ...row, ...values };
|
|
850
|
+
await appendChange(
|
|
851
|
+
d,
|
|
852
|
+
state.table,
|
|
853
|
+
"update",
|
|
854
|
+
String(row[OWNED_PK]),
|
|
855
|
+
toOwnerKey(row),
|
|
856
|
+
row,
|
|
857
|
+
after
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
return before.length;
|
|
862
|
+
}
|
|
863
|
+
const compiled = compileUpdate(state.table, state.wheres, values, dialect);
|
|
864
|
+
return (await driver.run(compiled.sql, compiled.params)).changes;
|
|
865
|
+
},
|
|
866
|
+
async delete() {
|
|
867
|
+
guardWrite();
|
|
868
|
+
assertHasWhere(state);
|
|
869
|
+
if (await resolveOwned(state, driver)) {
|
|
870
|
+
const before = await captureBeforeRows(state, dialect, driver);
|
|
871
|
+
const compiled2 = compileDelete(state.table, state.wheres, dialect);
|
|
872
|
+
await runOwnedWrite(driver, async (d) => {
|
|
873
|
+
await d.run(compiled2.sql, compiled2.params);
|
|
874
|
+
for (const row of before) {
|
|
875
|
+
await appendChange(
|
|
876
|
+
d,
|
|
877
|
+
state.table,
|
|
878
|
+
"delete",
|
|
879
|
+
String(row[OWNED_PK]),
|
|
880
|
+
toOwnerKey(row),
|
|
881
|
+
row,
|
|
882
|
+
null
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
return before.length;
|
|
887
|
+
}
|
|
888
|
+
const compiled = compileDelete(state.table, state.wheres, dialect);
|
|
889
|
+
return (await driver.run(compiled.sql, compiled.params)).changes;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
};
|
|
893
|
+
return build(initialState);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// packages/runtime/src/database/builder/index.ts
|
|
897
|
+
var TX_STATES = /* @__PURE__ */ new WeakMap();
|
|
898
|
+
function createCloudDb(driver, options = {}) {
|
|
899
|
+
const dialect = options.dialect ?? dialectFor(driver);
|
|
900
|
+
let txState = TX_STATES.get(driver);
|
|
901
|
+
if (txState === void 0) {
|
|
902
|
+
txState = { inTransaction: false };
|
|
903
|
+
TX_STATES.set(driver, txState);
|
|
904
|
+
}
|
|
905
|
+
const makeHandle = (handleOptions) => {
|
|
906
|
+
const guardWrite = () => {
|
|
907
|
+
if (txState !== void 0 && txState.inTransaction && !handleOptions.allowWriteDuringTx) {
|
|
908
|
+
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");
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
const handle = {
|
|
912
|
+
table(name) {
|
|
913
|
+
return createTableBuilder(name, driver, dialect, { guardWrite });
|
|
914
|
+
},
|
|
915
|
+
async transaction(fn) {
|
|
916
|
+
if (txState !== void 0 && txState.inTransaction) {
|
|
917
|
+
throw unsafeOperation("\u4E0D\u5141\u8BB8\u5D4C\u5957\u4E8B\u52A1\uFF1A\u4E8B\u52A1\u5185\u7981\u6B62\u518D\u6B21\u8C03\u7528 transaction()");
|
|
918
|
+
}
|
|
919
|
+
txState.inTransaction = true;
|
|
920
|
+
await driver.run("BEGIN");
|
|
921
|
+
const txHandle = makeHandle({ allowWriteDuringTx: true });
|
|
922
|
+
try {
|
|
923
|
+
const result = await fn(txHandle);
|
|
924
|
+
await driver.run("COMMIT");
|
|
925
|
+
return result;
|
|
926
|
+
} catch (error) {
|
|
927
|
+
await driver.run("ROLLBACK");
|
|
928
|
+
throw error;
|
|
929
|
+
} finally {
|
|
930
|
+
txState.inTransaction = false;
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
async query(sql, params) {
|
|
934
|
+
if (!Array.isArray(params)) {
|
|
935
|
+
throw unsafeOperation("query \u7684 params \u5FC5\u987B\u63D0\u4F9B\uFF08\u65E0\u53C2\u4F20 []\uFF09");
|
|
936
|
+
}
|
|
937
|
+
guardWrite();
|
|
938
|
+
assertReadOnlyQuery(sql);
|
|
939
|
+
return driver.all(sql, params);
|
|
940
|
+
},
|
|
941
|
+
async changes(table, query = {}) {
|
|
942
|
+
return readChanges(driver, table, query);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
return handle;
|
|
946
|
+
};
|
|
947
|
+
return makeHandle({ allowWriteDuringTx: false });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// packages/runtime/src/database/sdk/cloud.ts
|
|
951
|
+
var CLOUD_DB_SPEC = {
|
|
952
|
+
kind: "cloud-db",
|
|
953
|
+
rootMethod: "table",
|
|
954
|
+
stepMethods: ["select", "where", "orderBy", "limit", "offset"],
|
|
955
|
+
terminalMethods: ["get", "first", "count", "insert", "insertMany", "update", "delete"],
|
|
956
|
+
directMethods: ["query", "changes"],
|
|
957
|
+
transactionMethod: "transaction"
|
|
958
|
+
};
|
|
959
|
+
var WRITE_TERMINALS = /* @__PURE__ */ new Set(["insert", "insertMany", "update", "delete"]);
|
|
960
|
+
function errorWithCode(error) {
|
|
961
|
+
const code = typeof error === "object" && error !== null && typeof error.code === "string" ? error.code : void 0;
|
|
962
|
+
if (code !== void 0 && error instanceof Error) {
|
|
963
|
+
return new Error(`[${code}] ${error.message}`);
|
|
964
|
+
}
|
|
965
|
+
return error;
|
|
966
|
+
}
|
|
967
|
+
function createDbCapability(driver, options = {}) {
|
|
968
|
+
const db = createCloudDb(driver);
|
|
969
|
+
const activeTxs = /* @__PURE__ */ new Map();
|
|
970
|
+
const guardTxWrite = (txId, terminal) => {
|
|
971
|
+
if (txId === void 0) {
|
|
972
|
+
if (activeTxs.size > 0 && WRITE_TERMINALS.has(terminal)) {
|
|
973
|
+
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");
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (!activeTxs.has(txId)) {
|
|
978
|
+
throw unsafeOperation("\u5F15\u7528\u7684\u4E8B\u52A1\u5DF2\u7ED3\u675F\u6216\u4E0D\u5B58\u5728");
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
const runChain = async (request) => {
|
|
982
|
+
const tableName = request.rootArgs[0];
|
|
983
|
+
if (typeof tableName !== "string" && typeof tableName !== "number") {
|
|
984
|
+
throw unsafeOperation("table \u9700\u8981\u8868\u540D\u5B57\u7B26\u4E32");
|
|
985
|
+
}
|
|
986
|
+
let builder = db.table(String(tableName));
|
|
987
|
+
for (const step of request.steps) {
|
|
988
|
+
const invoke = builder[step.method];
|
|
989
|
+
if (typeof invoke !== "function") {
|
|
990
|
+
throw unsafeOperation(`\u4E91\u6570\u636E\u5E93\u4E0D\u652F\u6301\u94FE\u5F0F\u65B9\u6CD5 "${String(step.method)}"`);
|
|
991
|
+
}
|
|
992
|
+
builder = invoke(...step.args);
|
|
993
|
+
}
|
|
994
|
+
guardTxWrite(request.txId, request.terminal);
|
|
995
|
+
if (WRITE_TERMINALS.has(request.terminal) && options.beforeWrite !== void 0) {
|
|
996
|
+
await options.beforeWrite(driver);
|
|
997
|
+
}
|
|
998
|
+
const terminal = builder[request.terminal];
|
|
999
|
+
if (typeof terminal !== "function") {
|
|
1000
|
+
throw unsafeOperation(`\u4E91\u6570\u636E\u5E93\u4E0D\u652F\u6301\u7EC8\u503C\u65B9\u6CD5 "${request.terminal}"`);
|
|
1001
|
+
}
|
|
1002
|
+
return terminal(...request.terminalArgs);
|
|
1003
|
+
};
|
|
1004
|
+
const handler = async (method, args) => {
|
|
1005
|
+
try {
|
|
1006
|
+
switch (method) {
|
|
1007
|
+
case DB_RPC.query: {
|
|
1008
|
+
const [sql, params] = args;
|
|
1009
|
+
return await db.query(sql, params);
|
|
1010
|
+
}
|
|
1011
|
+
case DB_RPC.changes: {
|
|
1012
|
+
const [table, query] = args;
|
|
1013
|
+
return await db.changes(table, query);
|
|
1014
|
+
}
|
|
1015
|
+
case DB_RPC.begin: {
|
|
1016
|
+
if (activeTxs.size > 0) throw unsafeOperation("\u4E0D\u5141\u8BB8\u5D4C\u5957\u4E8B\u52A1");
|
|
1017
|
+
await driver.run("BEGIN");
|
|
1018
|
+
const txId = crypto.randomUUID();
|
|
1019
|
+
activeTxs.set(txId, true);
|
|
1020
|
+
return txId;
|
|
1021
|
+
}
|
|
1022
|
+
case DB_RPC.commit: {
|
|
1023
|
+
const [txId] = args;
|
|
1024
|
+
if (!activeTxs.has(txId)) throw unsafeOperation("\u5F15\u7528\u7684\u4E8B\u52A1\u5DF2\u7ED3\u675F\u6216\u4E0D\u5B58\u5728");
|
|
1025
|
+
activeTxs.delete(txId);
|
|
1026
|
+
await driver.run("COMMIT");
|
|
1027
|
+
return void 0;
|
|
1028
|
+
}
|
|
1029
|
+
case DB_RPC.rollback: {
|
|
1030
|
+
const [txId] = args;
|
|
1031
|
+
if (activeTxs.has(txId)) {
|
|
1032
|
+
activeTxs.delete(txId);
|
|
1033
|
+
await driver.run("ROLLBACK");
|
|
1034
|
+
}
|
|
1035
|
+
return void 0;
|
|
1036
|
+
}
|
|
1037
|
+
case DB_RPC.chain: {
|
|
1038
|
+
const request = args[0];
|
|
1039
|
+
return await runChain(request);
|
|
1040
|
+
}
|
|
1041
|
+
default:
|
|
1042
|
+
throw new Error(`\u672A\u77E5\u7684 cloud.db \u65B9\u6CD5 "${String(method)}"`);
|
|
1043
|
+
}
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
throw errorWithCode(error);
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
return {
|
|
1049
|
+
capabilities: [{ name: "db", value: { [CHAIN_CAPABILITY_KEY]: CLOUD_DB_SPEC } }],
|
|
1050
|
+
rpcHandlers: { db: handler }
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// packages/cli/src/sim/sql-engine.ts
|
|
1055
|
+
var SimDbError = class extends Error {
|
|
1056
|
+
constructor(code, message) {
|
|
1057
|
+
super(message);
|
|
1058
|
+
this.code = code;
|
|
1059
|
+
this.name = "SimDbError";
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
var ParamCursor = class {
|
|
1063
|
+
constructor(params) {
|
|
1064
|
+
this.params = params;
|
|
1065
|
+
}
|
|
1066
|
+
index = 0;
|
|
1067
|
+
take() {
|
|
1068
|
+
const v = this.params[this.index];
|
|
1069
|
+
this.index += 1;
|
|
1070
|
+
return v ?? null;
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
function ident(raw) {
|
|
1074
|
+
return raw.trim().replace(/^"|"$/g, "").trim();
|
|
1075
|
+
}
|
|
1076
|
+
function toNumber(raw) {
|
|
1077
|
+
const t = raw.trim();
|
|
1078
|
+
if (t === "?") return null;
|
|
1079
|
+
const n = Number(t);
|
|
1080
|
+
return Number.isNaN(n) ? null : n;
|
|
1081
|
+
}
|
|
1082
|
+
function stripQuotes(raw) {
|
|
1083
|
+
const t = raw.trim();
|
|
1084
|
+
if (/^'.*'$/.test(t)) return t.slice(1, -1);
|
|
1085
|
+
if (/^".*"$/.test(t)) return t.slice(1, -1);
|
|
1086
|
+
return t;
|
|
1087
|
+
}
|
|
1088
|
+
function parseCreateTable(ddl) {
|
|
1089
|
+
const m = /^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s*\(([\s\S]*)\)$/i.exec(
|
|
1090
|
+
ddl.trim()
|
|
1091
|
+
);
|
|
1092
|
+
if (m === null) return null;
|
|
1093
|
+
const name = ident(m[1]);
|
|
1094
|
+
const body = m[2];
|
|
1095
|
+
const columns = [];
|
|
1096
|
+
for (const part of body.split(",")) {
|
|
1097
|
+
const tokens = part.trim().split(/\s+/);
|
|
1098
|
+
const colName = ident(tokens[0] ?? "");
|
|
1099
|
+
const type = (tokens[1] ?? "TEXT").toUpperCase();
|
|
1100
|
+
const primaryKey = tokens.includes("PRIMARY") && tokens.includes("KEY");
|
|
1101
|
+
const autoincrement = primaryKey && tokens.includes("AUTOINCREMENT");
|
|
1102
|
+
columns.push({ name: colName, type, primaryKey, autoincrement });
|
|
1103
|
+
}
|
|
1104
|
+
return { name, columns };
|
|
1105
|
+
}
|
|
1106
|
+
function parseWhereClauses(whereRaw, cursor) {
|
|
1107
|
+
const clauses = [];
|
|
1108
|
+
for (const part of whereRaw.split(/\s+AND\s+/i)) {
|
|
1109
|
+
const t = part.trim();
|
|
1110
|
+
if (t.length === 0) continue;
|
|
1111
|
+
if (/\bIS\s+NULL\b/i.test(t)) {
|
|
1112
|
+
const col2 = ident(t.split(/\s+IS\s+NULL\b/i)[0] ?? "");
|
|
1113
|
+
clauses.push({ column: col2, operator: "IS NULL", value: null, list: false });
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (/\bIS\s+NOT\s+NULL\b/i.test(t)) {
|
|
1117
|
+
const col2 = ident(t.split(/\s+IS\s+NOT\s+NULL\b/i)[0] ?? "");
|
|
1118
|
+
clauses.push({ column: col2, operator: "IS NOT NULL", value: null, list: false });
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const opMatch = /^\s*((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s*(=|!=|>=|<=|>|<|like|in|not\s+in)\s*(.+)$/i.exec(
|
|
1122
|
+
t
|
|
1123
|
+
);
|
|
1124
|
+
if (opMatch === null) continue;
|
|
1125
|
+
const col = ident(opMatch[1] ?? "");
|
|
1126
|
+
const op = (opMatch[3] ?? opMatch[2] ?? "").toLowerCase();
|
|
1127
|
+
const rhs = (opMatch[4] ?? "").trim();
|
|
1128
|
+
if (op === "in" || op === "not in") {
|
|
1129
|
+
const inner = rhs.replace(/^\(|\)$/g, "");
|
|
1130
|
+
const items = splitListItems(inner);
|
|
1131
|
+
const list = items.map((item) => {
|
|
1132
|
+
if (item.trim() === "?") return cursor.take();
|
|
1133
|
+
return stripQuotes(item);
|
|
1134
|
+
});
|
|
1135
|
+
clauses.push({ column: col, operator: op, value: list, list: true });
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
let value;
|
|
1139
|
+
if (rhs === "?") {
|
|
1140
|
+
value = cursor.take();
|
|
1141
|
+
} else {
|
|
1142
|
+
value = stripQuotes(rhs);
|
|
1143
|
+
}
|
|
1144
|
+
clauses.push({ column: col, operator: op, value, list: false });
|
|
1145
|
+
}
|
|
1146
|
+
return clauses;
|
|
1147
|
+
}
|
|
1148
|
+
function matchWhere(clauses, row) {
|
|
1149
|
+
return clauses.every((c) => matchValue(row[c.column], c));
|
|
1150
|
+
}
|
|
1151
|
+
function splitListItems(inner) {
|
|
1152
|
+
const items = [];
|
|
1153
|
+
let depth = 0;
|
|
1154
|
+
let buffer = "";
|
|
1155
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
1156
|
+
const ch = inner[i];
|
|
1157
|
+
if (ch === "(") depth += 1;
|
|
1158
|
+
else if (ch === ")") depth -= 1;
|
|
1159
|
+
if (ch === "," && depth === 0) {
|
|
1160
|
+
items.push(buffer);
|
|
1161
|
+
buffer = "";
|
|
1162
|
+
} else {
|
|
1163
|
+
buffer += ch;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
items.push(buffer);
|
|
1167
|
+
return items;
|
|
1168
|
+
}
|
|
1169
|
+
function matchValue(value, clause) {
|
|
1170
|
+
const op = clause.operator;
|
|
1171
|
+
if (op === "IS NULL") return value === null || value === void 0;
|
|
1172
|
+
if (op === "IS NOT NULL") return value !== null && value !== void 0;
|
|
1173
|
+
if (op === "in") return clause.value.some((item) => value === item);
|
|
1174
|
+
if (op === "not in") return !clause.value.some((item) => value === item);
|
|
1175
|
+
if (op === "like") {
|
|
1176
|
+
if (typeof value !== "string") return false;
|
|
1177
|
+
const pattern = String(clause.value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
1178
|
+
return new RegExp(`^${pattern}$`, "s").test(value);
|
|
1179
|
+
}
|
|
1180
|
+
const left = value;
|
|
1181
|
+
const right = clause.value;
|
|
1182
|
+
switch (op) {
|
|
1183
|
+
case "=":
|
|
1184
|
+
return left === right;
|
|
1185
|
+
case "!=":
|
|
1186
|
+
return left !== right;
|
|
1187
|
+
case ">":
|
|
1188
|
+
return left !== null && right !== null && left > right;
|
|
1189
|
+
case ">=":
|
|
1190
|
+
return left !== null && right !== null && left >= right;
|
|
1191
|
+
case "<":
|
|
1192
|
+
return left !== null && right !== null && left < right;
|
|
1193
|
+
case "<=":
|
|
1194
|
+
return left !== null && right !== null && left <= right;
|
|
1195
|
+
default:
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function assertReadOnly(sql) {
|
|
1200
|
+
const stripped = sql.replace(/'[^']*(?:''[^']*)*'/g, "").replace(/"[^"]*(?:""[^"]*)*"/g, "").replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1201
|
+
if (stripped.includes(";")) throw new SimDbError("DB_UNSAFE_OP", "\u4EC5\u5141\u8BB8\u5355\u6761\u8BED\u53E5\uFF0C\u7981\u6B62\u591A\u8BED\u53E5\u5806\u53E0");
|
|
1202
|
+
const head = stripped.trim().replace(/^\(+/, "").trim().toUpperCase();
|
|
1203
|
+
if (!head.startsWith("SELECT") && !head.startsWith("WITH")) {
|
|
1204
|
+
throw new SimDbError("DB_UNSAFE_OP", "cloud.db.query \u4EC5\u5141\u8BB8\u53EA\u8BFB\u67E5\u8BE2\uFF08SELECT / WITH\uFF09");
|
|
1205
|
+
}
|
|
1206
|
+
if (/ATTACH|DETACH/.test(stripped.toUpperCase())) {
|
|
1207
|
+
throw new SimDbError("DB_UNSAFE_OP", "\u7981\u6B62\u8DE8\u5E93\u64CD\u4F5C\uFF08ATTACH / DETACH\uFF09");
|
|
1208
|
+
}
|
|
1209
|
+
if (/SQLITE_\w+/i.test(stripped) && !/SQLITE_MASTER\b/i.test(stripped)) {
|
|
1210
|
+
throw new SimDbError("DB_UNSAFE_OP", "\u7981\u6B62\u8BBF\u95EE\u7CFB\u7EDF\u8868\uFF08sqlite_*\uFF09");
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
var SimSqlEngine = class {
|
|
1214
|
+
tables = {};
|
|
1215
|
+
storage;
|
|
1216
|
+
memoryOnly;
|
|
1217
|
+
savepointDepth = 0;
|
|
1218
|
+
txSnapshot = null;
|
|
1219
|
+
constructor(options = {}) {
|
|
1220
|
+
this.storage = options.storage ?? { load: async () => null, save: async () => void 0 };
|
|
1221
|
+
this.memoryOnly = options.memoryOnly ?? false;
|
|
1222
|
+
}
|
|
1223
|
+
async load() {
|
|
1224
|
+
if (this.memoryOnly) return;
|
|
1225
|
+
const persisted = await this.storage.load();
|
|
1226
|
+
if (persisted !== null) this.tables = persisted;
|
|
1227
|
+
}
|
|
1228
|
+
async persist() {
|
|
1229
|
+
if (this.memoryOnly) return;
|
|
1230
|
+
await this.storage.save(this.tables);
|
|
1231
|
+
}
|
|
1232
|
+
ensureTable(name) {
|
|
1233
|
+
let table = this.tables[name];
|
|
1234
|
+
if (table === void 0) {
|
|
1235
|
+
table = { columns: [], rows: [], nextAutoincrement: 1 };
|
|
1236
|
+
this.tables[name] = table;
|
|
1237
|
+
}
|
|
1238
|
+
return table;
|
|
1239
|
+
}
|
|
1240
|
+
applyAutoincrement(table, row) {
|
|
1241
|
+
const pk = table.columns.find((c) => c.primaryKey && c.autoincrement);
|
|
1242
|
+
if (pk === void 0) return;
|
|
1243
|
+
const val = row[pk.name];
|
|
1244
|
+
if (val !== null && val !== void 0) {
|
|
1245
|
+
const n = Number(val);
|
|
1246
|
+
if (Number.isInteger(n) && n >= table.nextAutoincrement) table.nextAutoincrement = n + 1;
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
row[pk.name] = table.nextAutoincrement;
|
|
1250
|
+
table.nextAutoincrement += 1;
|
|
1251
|
+
}
|
|
1252
|
+
catalog() {
|
|
1253
|
+
const out = [];
|
|
1254
|
+
for (const [name] of Object.entries(this.tables)) {
|
|
1255
|
+
out.push({ type: "table", name, tbl_name: name });
|
|
1256
|
+
}
|
|
1257
|
+
return out;
|
|
1258
|
+
}
|
|
1259
|
+
async run(sql, params = []) {
|
|
1260
|
+
const stmt = sql.trim();
|
|
1261
|
+
if (/^BEGIN\s*$/i.test(stmt)) {
|
|
1262
|
+
this.txSnapshot = structuredClone(this.tables);
|
|
1263
|
+
return { changes: 0 };
|
|
1264
|
+
}
|
|
1265
|
+
if (/^COMMIT\s*$/i.test(stmt)) {
|
|
1266
|
+
this.txSnapshot = null;
|
|
1267
|
+
await this.persist();
|
|
1268
|
+
return { changes: 0 };
|
|
1269
|
+
}
|
|
1270
|
+
if (/^ROLLBACK\s*$/i.test(stmt)) {
|
|
1271
|
+
if (this.txSnapshot !== null) {
|
|
1272
|
+
this.tables = this.txSnapshot;
|
|
1273
|
+
this.txSnapshot = null;
|
|
1274
|
+
}
|
|
1275
|
+
await this.persist();
|
|
1276
|
+
return { changes: 0 };
|
|
1277
|
+
}
|
|
1278
|
+
if (/^SAVEPOINT\s+/i.test(stmt)) {
|
|
1279
|
+
this.savepointDepth += 1;
|
|
1280
|
+
return { changes: 0 };
|
|
1281
|
+
}
|
|
1282
|
+
if (/^RELEASE\s+SAVEPOINT\s+/i.test(stmt)) {
|
|
1283
|
+
this.savepointDepth = Math.max(0, this.savepointDepth - 1);
|
|
1284
|
+
return { changes: 0 };
|
|
1285
|
+
}
|
|
1286
|
+
if (/^ROLLBACK\s+TO\s+SAVEPOINT\s+/i.test(stmt)) return { changes: 0 };
|
|
1287
|
+
if (/^CREATE\s+TABLE/i.test(stmt)) {
|
|
1288
|
+
const parsed = parseCreateTable(stmt);
|
|
1289
|
+
if (parsed === null) throw new SimDbError("DB_UNSAFE_OP", `\u65E0\u6CD5\u89E3\u6790\u5EFA\u8868\u8BED\u53E5`);
|
|
1290
|
+
if (this.tables[parsed.name] === void 0) {
|
|
1291
|
+
this.tables[parsed.name] = { columns: parsed.columns, rows: [], nextAutoincrement: 1 };
|
|
1292
|
+
await this.persist();
|
|
1293
|
+
}
|
|
1294
|
+
return { changes: 0 };
|
|
1295
|
+
}
|
|
1296
|
+
if (/^INSERT\s+INTO/i.test(stmt)) return this.execInsert(stmt, params);
|
|
1297
|
+
if (/^UPDATE\s+/i.test(stmt)) return this.execUpdate(stmt, params);
|
|
1298
|
+
if (/^DELETE\s+FROM/i.test(stmt)) return this.execDelete(stmt, params);
|
|
1299
|
+
throw new SimDbError("DB_UNSAFE_OP", `sim SQL \u5F15\u64CE\u4E0D\u652F\u6301\u8BE5\u8BED\u53E5`);
|
|
1300
|
+
}
|
|
1301
|
+
execInsert(sql, params) {
|
|
1302
|
+
const match = /^INSERT\s+INTO\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s*\(([^)]*)\)\s*VALUES\s*([\s\S]+)$/i.exec(
|
|
1303
|
+
sql
|
|
1304
|
+
);
|
|
1305
|
+
if (match === null) throw new SimDbError("DB_UNSAFE_OP", "\u65E0\u6CD5\u89E3\u6790 INSERT");
|
|
1306
|
+
const tableName = ident(match[1]);
|
|
1307
|
+
const cols = match[3].split(",").map(ident);
|
|
1308
|
+
const valueBody = match[4].trim();
|
|
1309
|
+
const cursor = new ParamCursor(params);
|
|
1310
|
+
const table = this.ensureTable(tableName);
|
|
1311
|
+
let inserted = 0;
|
|
1312
|
+
for (const group of splitListItems(valueBody)) {
|
|
1313
|
+
const inner = group.trim().replace(/^\(|\)$/g, "");
|
|
1314
|
+
const values = splitListItems(inner).map(
|
|
1315
|
+
(item) => item.trim() === "?" ? cursor.take() : stripQuotes(item)
|
|
1316
|
+
);
|
|
1317
|
+
const row = {};
|
|
1318
|
+
cols.forEach((col, i) => {
|
|
1319
|
+
row[col] = values[i] ?? null;
|
|
1320
|
+
});
|
|
1321
|
+
this.applyAutoincrement(table, row);
|
|
1322
|
+
table.rows.push(row);
|
|
1323
|
+
inserted += 1;
|
|
1324
|
+
}
|
|
1325
|
+
void this.persist();
|
|
1326
|
+
return { changes: inserted };
|
|
1327
|
+
}
|
|
1328
|
+
execUpdate(sql, params) {
|
|
1329
|
+
const match = /^UPDATE\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s+SET\s+([\s\S]+?)\s+WHERE\s+([\s\S]+)$/i.exec(
|
|
1330
|
+
sql
|
|
1331
|
+
);
|
|
1332
|
+
if (match === null) throw new SimDbError("DB_UNSAFE_OP", "UPDATE \u5FC5\u987B\u5E26 WHERE");
|
|
1333
|
+
const tableName = ident(match[1]);
|
|
1334
|
+
const setRaw = match[3];
|
|
1335
|
+
const whereRaw = match[4];
|
|
1336
|
+
const cursor = new ParamCursor(params);
|
|
1337
|
+
const sets = splitListItems(setRaw).map((part) => {
|
|
1338
|
+
const [col, , mark] = part.trim().split(/\s+/);
|
|
1339
|
+
return { col: ident(col ?? ""), mark: mark ?? "?" };
|
|
1340
|
+
});
|
|
1341
|
+
const table = this.ensureTable(tableName);
|
|
1342
|
+
const boundSets = sets.map((set) => ({
|
|
1343
|
+
col: set.col,
|
|
1344
|
+
value: set.mark === "?" ? cursor.take() : stripQuotes(set.mark)
|
|
1345
|
+
}));
|
|
1346
|
+
const whereClauses = parseWhereClauses(whereRaw, cursor);
|
|
1347
|
+
let changes = 0;
|
|
1348
|
+
for (const row of table.rows) {
|
|
1349
|
+
if (!matchWhere(whereClauses, row)) continue;
|
|
1350
|
+
for (const set of boundSets) {
|
|
1351
|
+
row[set.col] = set.value;
|
|
1352
|
+
}
|
|
1353
|
+
changes += 1;
|
|
1354
|
+
}
|
|
1355
|
+
void this.persist();
|
|
1356
|
+
return { changes };
|
|
1357
|
+
}
|
|
1358
|
+
execDelete(sql, params) {
|
|
1359
|
+
const match = /^DELETE\s+FROM\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s+WHERE\s+([\s\S]+)$/i.exec(sql);
|
|
1360
|
+
if (match === null) throw new SimDbError("DB_UNSAFE_OP", "DELETE \u5FC5\u987B\u5E26 WHERE");
|
|
1361
|
+
const tableName = ident(match[1]);
|
|
1362
|
+
const whereRaw = match[3];
|
|
1363
|
+
const cursor = new ParamCursor(params);
|
|
1364
|
+
const table = this.ensureTable(tableName);
|
|
1365
|
+
const whereClauses = parseWhereClauses(whereRaw, cursor);
|
|
1366
|
+
const keep = [];
|
|
1367
|
+
let changes = 0;
|
|
1368
|
+
for (const row of table.rows) {
|
|
1369
|
+
if (!matchWhere(whereClauses, row)) keep.push(row);
|
|
1370
|
+
else changes += 1;
|
|
1371
|
+
}
|
|
1372
|
+
table.rows = keep;
|
|
1373
|
+
void this.persist();
|
|
1374
|
+
return { changes };
|
|
1375
|
+
}
|
|
1376
|
+
select(sql, params) {
|
|
1377
|
+
const m = /^SELECT\s+([\s\S]+?)\s+FROM\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))([\s\S]*)$/i.exec(
|
|
1378
|
+
sql.trim()
|
|
1379
|
+
);
|
|
1380
|
+
if (m === null) throw new SimDbError("DB_UNSAFE_OP", "\u65E0\u6CD5\u89E3\u6790 SELECT");
|
|
1381
|
+
const selectRaw = m[1].trim();
|
|
1382
|
+
const tableName = ident(m[2]);
|
|
1383
|
+
const tail = m[4] ?? "";
|
|
1384
|
+
const cursor = new ParamCursor(params);
|
|
1385
|
+
if (tableName.toLowerCase() === "sqlite_master") {
|
|
1386
|
+
return project(this.catalog(), selectRaw);
|
|
1387
|
+
}
|
|
1388
|
+
const table = this.ensureTable(tableName);
|
|
1389
|
+
const whereMatch = /\bWHERE\b/i.exec(tail);
|
|
1390
|
+
const orderMatch = /\bORDER\s+BY\b/i.exec(tail);
|
|
1391
|
+
const limitMatch = /\bLIMIT\b/i.exec(tail);
|
|
1392
|
+
const offsetMatch = /\bOFFSET\b/i.exec(tail);
|
|
1393
|
+
const whereRaw = whereMatch === null ? "" : tail.slice(
|
|
1394
|
+
whereMatch.index + whereMatch[0].length,
|
|
1395
|
+
indexAfter(whereMatch.index, [orderMatch, limitMatch, offsetMatch], tail)
|
|
1396
|
+
);
|
|
1397
|
+
let rows = table.rows;
|
|
1398
|
+
if (whereRaw.trim().length > 0) {
|
|
1399
|
+
const clauses = parseWhereClauses(whereRaw, cursor);
|
|
1400
|
+
rows = rows.filter((row) => matchWhere(clauses, row));
|
|
1401
|
+
}
|
|
1402
|
+
if (orderMatch !== null) {
|
|
1403
|
+
const orderRaw = tail.slice(
|
|
1404
|
+
orderMatch.index + orderMatch[0].length,
|
|
1405
|
+
indexAfter(orderMatch.index, [limitMatch, offsetMatch], tail)
|
|
1406
|
+
);
|
|
1407
|
+
const oc = /^\s*((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s+(asc|desc)/i.exec(orderRaw);
|
|
1408
|
+
if (oc !== null) {
|
|
1409
|
+
const col = ident(oc[1]);
|
|
1410
|
+
const dir = oc[3].toLowerCase();
|
|
1411
|
+
rows = [...rows].toSorted((a, b) => {
|
|
1412
|
+
const av = a[col];
|
|
1413
|
+
const bv = b[col];
|
|
1414
|
+
const cmp = av === bv ? 0 : av === null ? -1 : bv === null ? 1 : av > bv ? 1 : -1;
|
|
1415
|
+
return dir === "desc" ? -cmp : cmp;
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (limitMatch !== null) {
|
|
1420
|
+
const limRaw = tail.slice(
|
|
1421
|
+
limitMatch.index + limitMatch[0].length,
|
|
1422
|
+
indexAfter(limitMatch.index, [offsetMatch], tail)
|
|
1423
|
+
);
|
|
1424
|
+
const lim = limRaw.trim() === "?" ? cursor.take() : toNumber(limRaw);
|
|
1425
|
+
if (typeof lim === "number") rows = rows.slice(0, lim);
|
|
1426
|
+
}
|
|
1427
|
+
if (offsetMatch !== null) {
|
|
1428
|
+
const offRaw = tail.slice(
|
|
1429
|
+
offsetMatch.index + offsetMatch[0].length,
|
|
1430
|
+
indexAfter(offsetMatch.index, [limitMatch], tail)
|
|
1431
|
+
);
|
|
1432
|
+
const off = offRaw.trim() === "?" ? cursor.take() : toNumber(offRaw);
|
|
1433
|
+
if (typeof off === "number") rows = rows.slice(off);
|
|
1434
|
+
}
|
|
1435
|
+
return project(rows, selectRaw);
|
|
1436
|
+
}
|
|
1437
|
+
async all(sql, params = []) {
|
|
1438
|
+
assertReadOnly(sql);
|
|
1439
|
+
return this.select(sql, params);
|
|
1440
|
+
}
|
|
1441
|
+
async get(sql, params = []) {
|
|
1442
|
+
assertReadOnly(sql);
|
|
1443
|
+
return this.select(sql, params)[0] ?? null;
|
|
1444
|
+
}
|
|
1445
|
+
async close() {
|
|
1446
|
+
void this.savepointDepth;
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
function indexAfter(start, matches, tail) {
|
|
1450
|
+
const candidates = matches.filter((x) => x !== null && x.index > start).map((x) => x.index);
|
|
1451
|
+
const end = candidates.length === 0 ? -1 : Math.min(...candidates);
|
|
1452
|
+
return end === -1 ? tail.length : end;
|
|
1453
|
+
}
|
|
1454
|
+
function project(rows, selectRaw) {
|
|
1455
|
+
if (/^count\s*\(\s*\*/i.test(selectRaw) || /^count\s*\(\s*1\)/i.test(selectRaw)) {
|
|
1456
|
+
const alias = selectRaw.match(/\bAS\s+([A-Za-z_][A-Za-z0-9_]*)/i)?.[1] ?? "n";
|
|
1457
|
+
return [{ [alias]: rows.length }];
|
|
1458
|
+
}
|
|
1459
|
+
const constMatch = /^(\d+)\s+AS\s+([A-Za-z_][A-Za-z0-9_]*)/i.exec(selectRaw);
|
|
1460
|
+
if (constMatch !== null) {
|
|
1461
|
+
return [{ [constMatch[2]]: Number(constMatch[1]) }];
|
|
1462
|
+
}
|
|
1463
|
+
if (selectRaw.trim() === "*") return rows;
|
|
1464
|
+
const cols = selectRaw.split(",").map((s) => ident(s.trim()));
|
|
1465
|
+
return rows.map((row) => {
|
|
1466
|
+
const out = {};
|
|
1467
|
+
for (const col of cols) out[col] = row[col];
|
|
1468
|
+
return out;
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// packages/cli/src/sim/db.ts
|
|
1473
|
+
function toProjectDriver(engine) {
|
|
1474
|
+
return {
|
|
1475
|
+
engine: "sqlite",
|
|
1476
|
+
run: (sql, params = []) => engine.run(sql, params),
|
|
1477
|
+
all: (sql, params = []) => engine.all(sql, params),
|
|
1478
|
+
get: (sql, params = []) => engine.get(sql, params),
|
|
1479
|
+
close: () => engine.close()
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function createSimDbCapability(options = {}) {
|
|
1483
|
+
const engine = new SimSqlEngine({
|
|
1484
|
+
storage: options.storage ?? { load: async () => null, save: async () => void 0 },
|
|
1485
|
+
memoryOnly: options.storage === void 0
|
|
1486
|
+
});
|
|
1487
|
+
const driver = toProjectDriver(engine);
|
|
1488
|
+
const bundle = createDbCapability(driver);
|
|
1489
|
+
return { bundle, engine, driver };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// packages/cli/src/sim/storage.ts
|
|
1493
|
+
import { join as join3 } from "node:path";
|
|
1494
|
+
|
|
1495
|
+
// packages/runtime/src/storage/driver.ts
|
|
1496
|
+
var PROJECT_QUOTA_BYTES = 1024 * 1024 * 1024;
|
|
1497
|
+
var StorageError = class extends Error {
|
|
1498
|
+
constructor(status, code, message) {
|
|
1499
|
+
super(message);
|
|
1500
|
+
this.status = status;
|
|
1501
|
+
this.code = code;
|
|
1502
|
+
this.name = "StorageError";
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
var STORAGE_CODES = {
|
|
1506
|
+
invalidPath: "STORAGE_INVALID_PATH",
|
|
1507
|
+
notFound: "STORAGE_NOT_FOUND",
|
|
1508
|
+
quotaExceeded: "STORAGE_QUOTA_EXCEEDED"
|
|
1509
|
+
};
|
|
1510
|
+
function assertSafeStoragePath(path) {
|
|
1511
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1512
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u5B58\u50A8\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1513
|
+
}
|
|
1514
|
+
if (path.length > 1024) {
|
|
1515
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u5B58\u50A8\u8DEF\u5F84\u8FC7\u957F\uFF08\u4E0A\u9650 1024 \u5B57\u7B26\uFF09");
|
|
1516
|
+
}
|
|
1517
|
+
if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || /[\0\r\n]/.test(path)) {
|
|
1518
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u5B58\u50A8\u8DEF\u5F84\u5FC5\u987B\u662F\u9879\u76EE\u6876\u5185\u7684\u76F8\u5BF9\u8DEF\u5F84");
|
|
1519
|
+
}
|
|
1520
|
+
const segments = path.split("/");
|
|
1521
|
+
for (const segment of segments) {
|
|
1522
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
1523
|
+
throw new StorageError(
|
|
1524
|
+
400,
|
|
1525
|
+
STORAGE_CODES.invalidPath,
|
|
1526
|
+
`\u5B58\u50A8\u8DEF\u5F84\u542B\u975E\u6CD5\u6BB5 "${segment}"\uFF08\u7981\u6B62\u7A7A\u6BB5 / . / .. \u7A7F\u8D8A\uFF09`
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
if (/[\\]/.test(segment)) {
|
|
1530
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u5B58\u50A8\u8DEF\u5F84\u7981\u6B62\u53CD\u659C\u6760");
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return path;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// packages/runtime/src/storage/hmac-sha256.ts
|
|
1537
|
+
var K = new Uint32Array([
|
|
1538
|
+
1116352408,
|
|
1539
|
+
1899447441,
|
|
1540
|
+
3049323471,
|
|
1541
|
+
3921009573,
|
|
1542
|
+
961987163,
|
|
1543
|
+
1508970993,
|
|
1544
|
+
2453635748,
|
|
1545
|
+
2870763221,
|
|
1546
|
+
3624381080,
|
|
1547
|
+
310598401,
|
|
1548
|
+
607225278,
|
|
1549
|
+
1426881987,
|
|
1550
|
+
1925078388,
|
|
1551
|
+
2162078206,
|
|
1552
|
+
2614888103,
|
|
1553
|
+
3248222580,
|
|
1554
|
+
3835390401,
|
|
1555
|
+
4022224774,
|
|
1556
|
+
264347078,
|
|
1557
|
+
604807628,
|
|
1558
|
+
770255983,
|
|
1559
|
+
1249150122,
|
|
1560
|
+
1555081692,
|
|
1561
|
+
1996064986,
|
|
1562
|
+
2554220882,
|
|
1563
|
+
2821834349,
|
|
1564
|
+
2952996808,
|
|
1565
|
+
3210313671,
|
|
1566
|
+
3336571891,
|
|
1567
|
+
3584528711,
|
|
1568
|
+
113926993,
|
|
1569
|
+
338241895,
|
|
1570
|
+
666307205,
|
|
1571
|
+
773529912,
|
|
1572
|
+
1294757372,
|
|
1573
|
+
1396182291,
|
|
1574
|
+
1695183700,
|
|
1575
|
+
1986661051,
|
|
1576
|
+
2177026350,
|
|
1577
|
+
2456956037,
|
|
1578
|
+
2730485921,
|
|
1579
|
+
2820302411,
|
|
1580
|
+
3259730800,
|
|
1581
|
+
3345764771,
|
|
1582
|
+
3516065817,
|
|
1583
|
+
3600352804,
|
|
1584
|
+
4094571909,
|
|
1585
|
+
275423344,
|
|
1586
|
+
430227734,
|
|
1587
|
+
506948616,
|
|
1588
|
+
659060556,
|
|
1589
|
+
883997877,
|
|
1590
|
+
958139571,
|
|
1591
|
+
1322822218,
|
|
1592
|
+
1537002063,
|
|
1593
|
+
1747873779,
|
|
1594
|
+
1955562222,
|
|
1595
|
+
2024104815,
|
|
1596
|
+
2227730452,
|
|
1597
|
+
2361852424,
|
|
1598
|
+
2428436474,
|
|
1599
|
+
2756734187,
|
|
1600
|
+
3204031479,
|
|
1601
|
+
3329325298
|
|
1602
|
+
]);
|
|
1603
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
1604
|
+
function compress(h, block, w) {
|
|
1605
|
+
for (let i = 0; i < 16; i += 1) {
|
|
1606
|
+
const j = i * 4;
|
|
1607
|
+
w[i] = (block[j] ?? 0) << 24 | (block[j + 1] ?? 0) << 16 | (block[j + 2] ?? 0) << 8 | (block[j + 3] ?? 0);
|
|
1608
|
+
}
|
|
1609
|
+
for (let i = 16; i < 64; i += 1) {
|
|
1610
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
|
|
1611
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
|
|
1612
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
|
|
1613
|
+
}
|
|
1614
|
+
let [a, b, c, d, e, f, g, hh] = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]];
|
|
1615
|
+
for (let i = 0; i < 64; i += 1) {
|
|
1616
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
1617
|
+
const ch = e & f ^ ~e & g;
|
|
1618
|
+
const t1 = hh + S1 + ch + K[i] + w[i] | 0;
|
|
1619
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
1620
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
1621
|
+
const t2 = S0 + maj | 0;
|
|
1622
|
+
hh = g;
|
|
1623
|
+
g = f;
|
|
1624
|
+
f = e;
|
|
1625
|
+
e = d + t1 | 0;
|
|
1626
|
+
d = c;
|
|
1627
|
+
c = b;
|
|
1628
|
+
b = a;
|
|
1629
|
+
a = t1 + t2 | 0;
|
|
1630
|
+
}
|
|
1631
|
+
h[0] = h[0] + a | 0;
|
|
1632
|
+
h[1] = h[1] + b | 0;
|
|
1633
|
+
h[2] = h[2] + c | 0;
|
|
1634
|
+
h[3] = h[3] + d | 0;
|
|
1635
|
+
h[4] = h[4] + e | 0;
|
|
1636
|
+
h[5] = h[5] + f | 0;
|
|
1637
|
+
h[6] = h[6] + g | 0;
|
|
1638
|
+
h[7] = h[7] + hh | 0;
|
|
1639
|
+
}
|
|
1640
|
+
function sha256(data) {
|
|
1641
|
+
const h = new Uint32Array([
|
|
1642
|
+
1779033703,
|
|
1643
|
+
3144134277,
|
|
1644
|
+
1013904242,
|
|
1645
|
+
2773480762,
|
|
1646
|
+
1359893119,
|
|
1647
|
+
2600822924,
|
|
1648
|
+
528734635,
|
|
1649
|
+
1541459225
|
|
1650
|
+
]);
|
|
1651
|
+
const bitLength = data.length * 8;
|
|
1652
|
+
const paddedLength = Math.ceil((data.length + 9) / 64) * 64;
|
|
1653
|
+
const padded = new Uint8Array(paddedLength);
|
|
1654
|
+
padded.set(data);
|
|
1655
|
+
padded[data.length] = 128;
|
|
1656
|
+
const view = new DataView(padded.buffer);
|
|
1657
|
+
view.setUint32(paddedLength - 4, bitLength >>> 0, false);
|
|
1658
|
+
view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296), false);
|
|
1659
|
+
const w = new Uint32Array(64);
|
|
1660
|
+
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
1661
|
+
compress(h, padded.subarray(offset, offset + 64), w);
|
|
1662
|
+
}
|
|
1663
|
+
const out = new Uint8Array(32);
|
|
1664
|
+
const outView = new DataView(out.buffer);
|
|
1665
|
+
for (let i = 0; i < 8; i += 1) outView.setUint32(i * 4, h[i], false);
|
|
1666
|
+
return out;
|
|
1667
|
+
}
|
|
1668
|
+
function toBytes(input) {
|
|
1669
|
+
if (typeof input !== "string") return input;
|
|
1670
|
+
return new TextEncoder().encode(input);
|
|
1671
|
+
}
|
|
1672
|
+
function hmacSha256(key, message) {
|
|
1673
|
+
const blockSize = 64;
|
|
1674
|
+
let keyBytes = toBytes(key);
|
|
1675
|
+
if (keyBytes.length > blockSize) keyBytes = sha256(keyBytes);
|
|
1676
|
+
const padded = new Uint8Array(blockSize);
|
|
1677
|
+
padded.set(keyBytes);
|
|
1678
|
+
const inner = new Uint8Array(blockSize);
|
|
1679
|
+
const outer = new Uint8Array(blockSize);
|
|
1680
|
+
for (let i = 0; i < blockSize; i += 1) {
|
|
1681
|
+
inner[i] = padded[i] ^ 54;
|
|
1682
|
+
outer[i] = padded[i] ^ 92;
|
|
1683
|
+
}
|
|
1684
|
+
const innerInput = new Uint8Array(blockSize + toBytes(message).length);
|
|
1685
|
+
innerInput.set(inner);
|
|
1686
|
+
innerInput.set(toBytes(message), blockSize);
|
|
1687
|
+
const innerHash = sha256(innerInput);
|
|
1688
|
+
const outerInput = new Uint8Array(blockSize + 32);
|
|
1689
|
+
outerInput.set(outer);
|
|
1690
|
+
outerInput.set(innerHash, blockSize);
|
|
1691
|
+
return sha256(outerInput);
|
|
1692
|
+
}
|
|
1693
|
+
function hmacSha256Hex(key, message) {
|
|
1694
|
+
return [...hmacSha256(key, message)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// packages/runtime/src/storage/signature.ts
|
|
1698
|
+
var DEFAULT_SIGN_TTL_SECONDS = 15 * 60;
|
|
1699
|
+
var SIGN_EXPIRES_KEY = "x-expires";
|
|
1700
|
+
var SIGN_SIGNATURE_KEY = "x-signature";
|
|
1701
|
+
function sign(secret, projectId, path, expires) {
|
|
1702
|
+
const payload = `${projectId}|${path}|${expires}`;
|
|
1703
|
+
return hmacSha256Hex(secret, payload);
|
|
1704
|
+
}
|
|
1705
|
+
function signDownloadUrl(config, projectId, path, ttlSeconds = DEFAULT_SIGN_TTL_SECONDS) {
|
|
1706
|
+
const expires = Math.floor(Date.now() / 1e3) + ttlSeconds;
|
|
1707
|
+
const base = `${config.baseUrl}/api/v1/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}`;
|
|
1708
|
+
const signature = sign(config.secret, projectId, path, expires);
|
|
1709
|
+
return `${base}?${SIGN_EXPIRES_KEY}=${expires}&${SIGN_SIGNATURE_KEY}=${signature}`;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// packages/runtime/src/storage/cloud.ts
|
|
1713
|
+
var DEFAULT_VISIBILITY = "private";
|
|
1714
|
+
function asBytes(data) {
|
|
1715
|
+
if (typeof data === "string") return new TextEncoder().encode(data);
|
|
1716
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
1717
|
+
if (data instanceof Uint8Array) return data;
|
|
1718
|
+
const candidate = data;
|
|
1719
|
+
if (candidate.data instanceof Uint8Array) return candidate.data;
|
|
1720
|
+
throw new StorageError(400, STORAGE_CODES.invalidPath, "\u4E0D\u652F\u6301\u7684\u5B58\u50A8\u6570\u636E\u7C7B\u578B");
|
|
1721
|
+
}
|
|
1722
|
+
function toStoredFile(meta) {
|
|
1723
|
+
return {
|
|
1724
|
+
path: meta.path,
|
|
1725
|
+
size: meta.size,
|
|
1726
|
+
visibility: meta.visibility,
|
|
1727
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType },
|
|
1728
|
+
...meta.updatedAt === void 0 ? {} : { updatedAt: meta.updatedAt }
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
function createStorageCapability(driver, signer, projectId) {
|
|
1732
|
+
const handler = async (method, args) => {
|
|
1733
|
+
switch (method) {
|
|
1734
|
+
case "upload": {
|
|
1735
|
+
const [path, data] = args;
|
|
1736
|
+
return toStoredFile(
|
|
1737
|
+
await driver.put(projectId, path, asBytes(data), { visibility: DEFAULT_VISIBILITY })
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
case "get": {
|
|
1741
|
+
const [path] = args;
|
|
1742
|
+
return (await driver.get(projectId, path)).data;
|
|
1743
|
+
}
|
|
1744
|
+
case "remove": {
|
|
1745
|
+
const [path] = args;
|
|
1746
|
+
await driver.remove(projectId, path);
|
|
1747
|
+
return void 0;
|
|
1748
|
+
}
|
|
1749
|
+
case "list": {
|
|
1750
|
+
const [prefix] = args;
|
|
1751
|
+
return (await driver.list(projectId, prefix)).map(toStoredFile);
|
|
1752
|
+
}
|
|
1753
|
+
case "getSignedUrl": {
|
|
1754
|
+
const [path, ttlSeconds] = args;
|
|
1755
|
+
return signDownloadUrl(signer, projectId, path, ttlSeconds);
|
|
1756
|
+
}
|
|
1757
|
+
default:
|
|
1758
|
+
throw new StorageError(
|
|
1759
|
+
400,
|
|
1760
|
+
"STORAGE_INVALID_METHOD",
|
|
1761
|
+
`\u672A\u77E5\u7684 cloud.storage \u65B9\u6CD5 "${method}"`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
return {
|
|
1766
|
+
capabilities: [{ name: "storage", value: { [RPC_CAPABILITY_KEY]: true } }],
|
|
1767
|
+
rpcHandlers: { storage: handler }
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// packages/runtime/src/storage/driver/local.ts
|
|
1772
|
+
import { mkdir, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
1773
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1774
|
+
var META_SUFFIX = ".adep-meta.json";
|
|
1775
|
+
function createLocalStorageDriver(options) {
|
|
1776
|
+
const quotaBytes = options.quotaBytes ?? PROJECT_QUOTA_BYTES;
|
|
1777
|
+
const bucketDir = (projectId) => join2(options.dir, projectId);
|
|
1778
|
+
const objectPath = (projectId, path) => join2(bucketDir(projectId), path);
|
|
1779
|
+
const metaPath = (projectId, path) => join2(dirname2(objectPath(projectId, path)), `${path.split("/").pop() ?? ""}${META_SUFFIX}`);
|
|
1780
|
+
const readStoredMeta = async (projectId, path) => {
|
|
1781
|
+
const raw = await readFile(metaPath(projectId, path), "utf8");
|
|
1782
|
+
return JSON.parse(raw);
|
|
1783
|
+
};
|
|
1784
|
+
const writeStoredMeta = async (projectId, path, meta) => {
|
|
1785
|
+
const payload = {
|
|
1786
|
+
visibility: meta.visibility,
|
|
1787
|
+
size: meta.size,
|
|
1788
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1789
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType }
|
|
1790
|
+
};
|
|
1791
|
+
await writeFile(metaPath(projectId, path), JSON.stringify(payload), "utf8");
|
|
1792
|
+
};
|
|
1793
|
+
const totalSize = async (projectId) => {
|
|
1794
|
+
const root = bucketDir(projectId);
|
|
1795
|
+
let total = 0;
|
|
1796
|
+
const walk = async (dir) => {
|
|
1797
|
+
let entries;
|
|
1798
|
+
try {
|
|
1799
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1800
|
+
} catch {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const tasks = [];
|
|
1804
|
+
for (const entry of entries) {
|
|
1805
|
+
const full = join2(dir, entry.name);
|
|
1806
|
+
if (entry.isDirectory()) {
|
|
1807
|
+
tasks.push(walk(full));
|
|
1808
|
+
} else if (entry.isFile() && !entry.name.endsWith(META_SUFFIX)) {
|
|
1809
|
+
tasks.push(
|
|
1810
|
+
(async () => {
|
|
1811
|
+
try {
|
|
1812
|
+
const info = await stat(full);
|
|
1813
|
+
total += info.size;
|
|
1814
|
+
} catch {
|
|
1815
|
+
}
|
|
1816
|
+
})()
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
await Promise.all(tasks);
|
|
1821
|
+
};
|
|
1822
|
+
await walk(root);
|
|
1823
|
+
return total;
|
|
1824
|
+
};
|
|
1825
|
+
return {
|
|
1826
|
+
async put(projectId, path, data, putOptions) {
|
|
1827
|
+
assertSafeStoragePath(path);
|
|
1828
|
+
if (!(data instanceof Uint8Array)) {
|
|
1829
|
+
throw new StorageError(400, "STORAGE_INVALID_PAYLOAD", "\u5199\u5165\u5185\u5BB9\u5FC5\u987B\u662F\u5B57\u8282\u6570\u7EC4");
|
|
1830
|
+
}
|
|
1831
|
+
const used = await totalSize(projectId);
|
|
1832
|
+
let existingSize = 0;
|
|
1833
|
+
try {
|
|
1834
|
+
existingSize = (await stat(objectPath(projectId, path))).size;
|
|
1835
|
+
} catch {
|
|
1836
|
+
existingSize = 0;
|
|
1837
|
+
}
|
|
1838
|
+
const projected = used - existingSize + data.byteLength;
|
|
1839
|
+
if (options.quotaCheck !== void 0) {
|
|
1840
|
+
await options.quotaCheck(projectId, projected);
|
|
1841
|
+
} else if (projected > quotaBytes) {
|
|
1842
|
+
throw new StorageError(
|
|
1843
|
+
413,
|
|
1844
|
+
STORAGE_CODES.quotaExceeded,
|
|
1845
|
+
`\u9879\u76EE\u5B58\u50A8\u7A7A\u95F4\u4E0D\u8DB3\uFF1A\u914D\u989D ${quotaBytes} \u5B57\u8282\u5DF2\u7528\u5C3D\uFF08STORAGE_QUOTA_EXCEEDED\uFF09`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
const dest = objectPath(projectId, path);
|
|
1849
|
+
await mkdir(dirname2(dest), { recursive: true });
|
|
1850
|
+
await writeFile(dest, data);
|
|
1851
|
+
await writeStoredMeta(projectId, path, {
|
|
1852
|
+
visibility: putOptions.visibility,
|
|
1853
|
+
size: data.byteLength,
|
|
1854
|
+
...putOptions.contentType === void 0 ? {} : { contentType: putOptions.contentType }
|
|
1855
|
+
});
|
|
1856
|
+
const meta = await readStoredMeta(projectId, path);
|
|
1857
|
+
return {
|
|
1858
|
+
path,
|
|
1859
|
+
size: meta.size,
|
|
1860
|
+
visibility: meta.visibility,
|
|
1861
|
+
updatedAt: meta.updatedAt,
|
|
1862
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType }
|
|
1863
|
+
};
|
|
1864
|
+
},
|
|
1865
|
+
async get(projectId, path) {
|
|
1866
|
+
assertSafeStoragePath(path);
|
|
1867
|
+
let meta;
|
|
1868
|
+
try {
|
|
1869
|
+
meta = await readStoredMeta(projectId, path);
|
|
1870
|
+
} catch {
|
|
1871
|
+
throw new StorageError(404, STORAGE_CODES.notFound, `\u6587\u4EF6 "${path}" \u4E0D\u5B58\u5728`);
|
|
1872
|
+
}
|
|
1873
|
+
const data = await readFile(objectPath(projectId, path));
|
|
1874
|
+
const result = {
|
|
1875
|
+
path,
|
|
1876
|
+
size: meta.size,
|
|
1877
|
+
visibility: meta.visibility,
|
|
1878
|
+
updatedAt: meta.updatedAt,
|
|
1879
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType }
|
|
1880
|
+
};
|
|
1881
|
+
return { data, meta: result };
|
|
1882
|
+
},
|
|
1883
|
+
async getMeta(projectId, path) {
|
|
1884
|
+
assertSafeStoragePath(path);
|
|
1885
|
+
let meta;
|
|
1886
|
+
try {
|
|
1887
|
+
meta = await readStoredMeta(projectId, path);
|
|
1888
|
+
} catch {
|
|
1889
|
+
throw new StorageError(404, STORAGE_CODES.notFound, `\u6587\u4EF6 "${path}" \u4E0D\u5B58\u5728`);
|
|
1890
|
+
}
|
|
1891
|
+
const result = {
|
|
1892
|
+
path,
|
|
1893
|
+
size: meta.size,
|
|
1894
|
+
visibility: meta.visibility,
|
|
1895
|
+
updatedAt: meta.updatedAt,
|
|
1896
|
+
...meta.contentType === void 0 ? {} : { contentType: meta.contentType }
|
|
1897
|
+
};
|
|
1898
|
+
return result;
|
|
1899
|
+
},
|
|
1900
|
+
async remove(projectId, path) {
|
|
1901
|
+
assertSafeStoragePath(path);
|
|
1902
|
+
let exists = true;
|
|
1903
|
+
try {
|
|
1904
|
+
await stat(objectPath(projectId, path));
|
|
1905
|
+
} catch {
|
|
1906
|
+
exists = false;
|
|
1907
|
+
}
|
|
1908
|
+
if (!exists) {
|
|
1909
|
+
throw new StorageError(404, STORAGE_CODES.notFound, `\u6587\u4EF6 "${path}" \u4E0D\u5B58\u5728`);
|
|
1910
|
+
}
|
|
1911
|
+
await rm(objectPath(projectId, path), { force: true });
|
|
1912
|
+
await unlink(metaPath(projectId, path)).catch(() => void 0);
|
|
1913
|
+
},
|
|
1914
|
+
async list(projectId, prefix) {
|
|
1915
|
+
if (prefix !== void 0 && prefix !== "") {
|
|
1916
|
+
const segments = prefix.split("/");
|
|
1917
|
+
if (prefix.startsWith("/") || /[\\]/.test(prefix) || segments.some((segment) => segment === "..")) {
|
|
1918
|
+
throw new StorageError(
|
|
1919
|
+
400,
|
|
1920
|
+
STORAGE_CODES.invalidPath,
|
|
1921
|
+
`\u975E\u6CD5\u524D\u7F00 "${prefix}"\uFF08\u7981\u6B62 / \u5F00\u5934\u3001\u53CD\u659C\u6760\u6216 .. \u7A7F\u8D8A\uFF09`
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
const root = bucketDir(projectId);
|
|
1926
|
+
const metas = [];
|
|
1927
|
+
const walk = async (dir, relative) => {
|
|
1928
|
+
let entries;
|
|
1929
|
+
try {
|
|
1930
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1931
|
+
} catch {
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
const tasks = [];
|
|
1935
|
+
for (const entry of entries) {
|
|
1936
|
+
if (entry.name.endsWith(META_SUFFIX)) continue;
|
|
1937
|
+
const full = join2(dir, entry.name);
|
|
1938
|
+
const rel = relative === "" ? entry.name : `${relative}/${entry.name}`;
|
|
1939
|
+
if (entry.isFile()) {
|
|
1940
|
+
if (prefix !== void 0 && !rel.startsWith(prefix)) continue;
|
|
1941
|
+
tasks.push(
|
|
1942
|
+
(async () => {
|
|
1943
|
+
try {
|
|
1944
|
+
const payload = JSON.parse(
|
|
1945
|
+
await readFile(full + META_SUFFIX, "utf8")
|
|
1946
|
+
);
|
|
1947
|
+
metas.push({
|
|
1948
|
+
path: rel,
|
|
1949
|
+
size: payload.size,
|
|
1950
|
+
visibility: payload.visibility,
|
|
1951
|
+
updatedAt: payload.updatedAt,
|
|
1952
|
+
...payload.contentType === void 0 ? {} : { contentType: payload.contentType }
|
|
1953
|
+
});
|
|
1954
|
+
} catch {
|
|
1955
|
+
}
|
|
1956
|
+
})()
|
|
1957
|
+
);
|
|
1958
|
+
} else if (entry.isDirectory()) {
|
|
1959
|
+
if (prefix !== void 0 && !rel.startsWith(prefix)) {
|
|
1960
|
+
const subIncluded = prefix.startsWith(rel + "/") || rel.startsWith(prefix);
|
|
1961
|
+
if (!subIncluded) continue;
|
|
1962
|
+
}
|
|
1963
|
+
tasks.push(walk(full, rel));
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
await Promise.all(tasks);
|
|
1967
|
+
};
|
|
1968
|
+
await walk(root, "");
|
|
1969
|
+
metas.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
1970
|
+
return metas;
|
|
1971
|
+
},
|
|
1972
|
+
async usage(projectId) {
|
|
1973
|
+
return totalSize(projectId);
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// packages/cli/src/sim/storage.ts
|
|
1979
|
+
function createSimStorageCapability(options) {
|
|
1980
|
+
const dir = join3(options.cwd, ".adep", "sim", "storage");
|
|
1981
|
+
const driver = createLocalStorageDriver({ dir });
|
|
1982
|
+
const signer = {
|
|
1983
|
+
secret: options.secret ?? "adep-sim-secret",
|
|
1984
|
+
baseUrl: options.baseUrl ?? "http://127.0.0.1:8787"
|
|
1985
|
+
};
|
|
1986
|
+
const bundle = createStorageCapability(driver, signer, options.projectId ?? "sim-project");
|
|
1987
|
+
return { bundle, driver };
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// packages/cli/src/sim/realtime.ts
|
|
1991
|
+
var SimRealtime = class {
|
|
1992
|
+
seq = 0;
|
|
1993
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
1994
|
+
/** 向某 channel 广播一条消息;返回被投递的连接数(单进程内)。 */
|
|
1995
|
+
publish(channel, data, publisher) {
|
|
1996
|
+
this.seq += 1;
|
|
1997
|
+
const message = {
|
|
1998
|
+
channel,
|
|
1999
|
+
seq: this.seq,
|
|
2000
|
+
data,
|
|
2001
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2002
|
+
...publisher === void 0 ? {} : { publisher }
|
|
2003
|
+
};
|
|
2004
|
+
const set = this.subscribers.get(channel);
|
|
2005
|
+
if (set === void 0) return { delivered: 0 };
|
|
2006
|
+
let delivered = 0;
|
|
2007
|
+
for (const listener of set) {
|
|
2008
|
+
listener(message);
|
|
2009
|
+
delivered += 1;
|
|
2010
|
+
}
|
|
2011
|
+
return { delivered };
|
|
2012
|
+
}
|
|
2013
|
+
/** 订阅某 channel;返回退订函数。 */
|
|
2014
|
+
subscribe(channel, listener) {
|
|
2015
|
+
let set = this.subscribers.get(channel);
|
|
2016
|
+
if (set === void 0) {
|
|
2017
|
+
set = /* @__PURE__ */ new Set();
|
|
2018
|
+
this.subscribers.set(channel, set);
|
|
2019
|
+
}
|
|
2020
|
+
set.add(listener);
|
|
2021
|
+
return () => {
|
|
2022
|
+
set?.delete(listener);
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
/** 当前有订阅者的 channel 数(调试用)。 */
|
|
2026
|
+
channelCount() {
|
|
2027
|
+
return this.subscribers.size;
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
var MAX_BUFFERED_MESSAGES = 64;
|
|
2031
|
+
var MAX_SUBSCRIPTIONS = 64;
|
|
2032
|
+
var REALTIME_CODES = REALTIME_CAPABILITY_CODES;
|
|
2033
|
+
function realtimeError(code, message) {
|
|
2034
|
+
return new Error(`[${code}] ${message}`);
|
|
2035
|
+
}
|
|
2036
|
+
function createSimRealtimeCapability() {
|
|
2037
|
+
const sim = new SimRealtime();
|
|
2038
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
2039
|
+
const disposers = /* @__PURE__ */ new Map();
|
|
2040
|
+
let seq = 0;
|
|
2041
|
+
const handler = async (method, args) => {
|
|
2042
|
+
switch (method) {
|
|
2043
|
+
case "publish": {
|
|
2044
|
+
if (args.length > 2) {
|
|
2045
|
+
throw realtimeError(
|
|
2046
|
+
REALTIME_CODES.unsupportedArg,
|
|
2047
|
+
"\u672C\u5730\u5355\u8FDB\u7A0B\u5185\u5B58\u5E7F\u64AD\u65E0\u8FDE\u63A5\u8EAB\u4EFD\uFF0C\u4E0D\u652F\u6301 publish \u7684 except \u5B9E\u53C2"
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
const [channel, data] = args;
|
|
2051
|
+
return sim.publish(channel, data);
|
|
2052
|
+
}
|
|
2053
|
+
case "subscribe": {
|
|
2054
|
+
if (buffers.size >= MAX_SUBSCRIPTIONS) {
|
|
2055
|
+
throw realtimeError(
|
|
2056
|
+
REALTIME_CODES.tooManySubscriptions,
|
|
2057
|
+
`\u672C\u5730\u8BA2\u9605\u6570\u5DF2\u8FBE\u4E0A\u9650 ${String(MAX_SUBSCRIPTIONS)}\uFF0C\u8BF7\u5148 unsubscribe`
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
const [channel] = args;
|
|
2061
|
+
seq += 1;
|
|
2062
|
+
const id = `sub-${String(seq)}`;
|
|
2063
|
+
const buffer = [];
|
|
2064
|
+
const off = sim.subscribe(channel, (msg) => {
|
|
2065
|
+
buffer.push(msg);
|
|
2066
|
+
if (buffer.length > MAX_BUFFERED_MESSAGES) buffer.shift();
|
|
2067
|
+
});
|
|
2068
|
+
buffers.set(id, buffer);
|
|
2069
|
+
disposers.set(id, off);
|
|
2070
|
+
return { subscription: id, channel };
|
|
2071
|
+
}
|
|
2072
|
+
case "receive": {
|
|
2073
|
+
const buffer = buffers.get(args[0]);
|
|
2074
|
+
if (buffer === void 0) {
|
|
2075
|
+
throw realtimeError(
|
|
2076
|
+
REALTIME_CODES.subscriptionNotFound,
|
|
2077
|
+
`\u672C\u5730\u8BA2\u9605 "${String(args[0])}" \u4E0D\u5B58\u5728\u6216\u5DF2\u9000\u8BA2`
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
return buffer.splice(0, buffer.length);
|
|
2081
|
+
}
|
|
2082
|
+
case "unsubscribe": {
|
|
2083
|
+
const id = args[0];
|
|
2084
|
+
const off = disposers.get(id);
|
|
2085
|
+
if (off === void 0) {
|
|
2086
|
+
throw realtimeError(
|
|
2087
|
+
REALTIME_CODES.subscriptionNotFound,
|
|
2088
|
+
`\u672C\u5730\u8BA2\u9605 "${id}" \u4E0D\u5B58\u5728\u6216\u5DF2\u9000\u8BA2`
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
off();
|
|
2092
|
+
disposers.delete(id);
|
|
2093
|
+
buffers.delete(id);
|
|
2094
|
+
return { removed: true };
|
|
2095
|
+
}
|
|
2096
|
+
default:
|
|
2097
|
+
throw realtimeError(
|
|
2098
|
+
REALTIME_CODES.invalidMethod,
|
|
2099
|
+
`\u672A\u77E5\u7684 cloud.realtime \u65B9\u6CD5 "${method}"\uFF08\u53EF\u7528\uFF1A${REALTIME_CAPABILITY_METHODS.join(" / ")}\uFF09`
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
return {
|
|
2104
|
+
bundle: {
|
|
2105
|
+
capabilities: [{ name: "realtime", value: { [RPC_CAPABILITY_KEY]: true } }],
|
|
2106
|
+
rpcHandlers: { realtime: handler }
|
|
2107
|
+
},
|
|
2108
|
+
realtime: sim
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// packages/cli/src/sim/runtime.ts
|
|
2113
|
+
function simDir(cwd) {
|
|
2114
|
+
return join4(cwd, ".adep", "sim");
|
|
2115
|
+
}
|
|
2116
|
+
function mergeBundles(bundles) {
|
|
2117
|
+
const capabilities = [];
|
|
2118
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2119
|
+
for (const bundle of bundles) {
|
|
2120
|
+
for (const cap of bundle.capabilities) {
|
|
2121
|
+
const existing = byName.get(cap.name);
|
|
2122
|
+
if (existing !== void 0) capabilities.splice(capabilities.indexOf(existing), 1);
|
|
2123
|
+
byName.set(cap.name, cap);
|
|
2124
|
+
capabilities.push(cap);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
const rpcHandlers = {};
|
|
2128
|
+
for (const bundle of bundles) {
|
|
2129
|
+
for (const [name, handler] of Object.entries(bundle.rpcHandlers)) {
|
|
2130
|
+
rpcHandlers[name] = handler;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
return { capabilities, rpcHandlers };
|
|
2134
|
+
}
|
|
2135
|
+
function jsonStorage(cwd) {
|
|
2136
|
+
const file = join4(simDir(cwd), "db.json");
|
|
2137
|
+
return {
|
|
2138
|
+
async load() {
|
|
2139
|
+
try {
|
|
2140
|
+
const raw = await readFile2(file, "utf8");
|
|
2141
|
+
return JSON.parse(raw);
|
|
2142
|
+
} catch {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
},
|
|
2146
|
+
async save(tables) {
|
|
2147
|
+
await mkdir2(dirname3(file), { recursive: true });
|
|
2148
|
+
await writeFile2(file, JSON.stringify(tables), "utf8");
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
async function createSimRuntime(options) {
|
|
2153
|
+
const db = createSimDbCapability({
|
|
2154
|
+
storage: options.dbStorage ?? jsonStorage(options.cwd),
|
|
2155
|
+
...options.log === void 0 ? {} : { log: options.log }
|
|
2156
|
+
});
|
|
2157
|
+
const storage = createSimStorageCapability({
|
|
2158
|
+
cwd: options.cwd,
|
|
2159
|
+
...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl },
|
|
2160
|
+
projectId: options.projectId ?? "local"
|
|
2161
|
+
});
|
|
2162
|
+
const simRealtime = createSimRealtimeCapability();
|
|
2163
|
+
const bundle = mergeBundles([db.bundle, storage.bundle, simRealtime.bundle]);
|
|
2164
|
+
await db.engine.load();
|
|
2165
|
+
return {
|
|
2166
|
+
bundle,
|
|
2167
|
+
realtime: simRealtime.realtime,
|
|
2168
|
+
db,
|
|
2169
|
+
dispose: async () => {
|
|
2170
|
+
await db.driver.close();
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// packages/cli/src/sim/env.ts
|
|
2176
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
2177
|
+
import { join as join5 } from "node:path";
|
|
2178
|
+
function parseEnv(content) {
|
|
2179
|
+
const env = {};
|
|
2180
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
2181
|
+
const line = raw.trim();
|
|
2182
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
2183
|
+
const eq = line.indexOf("=");
|
|
2184
|
+
if (eq === -1) continue;
|
|
2185
|
+
const key = line.slice(0, eq).trim();
|
|
2186
|
+
if (key.length === 0) continue;
|
|
2187
|
+
env[key] = line.slice(eq + 1).trim();
|
|
2188
|
+
}
|
|
2189
|
+
return env;
|
|
2190
|
+
}
|
|
2191
|
+
async function loadSimEnv(cwd, config = {}) {
|
|
2192
|
+
const envRoot = await readFile3(join5(cwd, ".env"), "utf8").catch(() => "");
|
|
2193
|
+
const envLocal = await readFile3(join5(cwd, ".env.local"), "utf8").catch(() => "");
|
|
2194
|
+
const merged = { ...parseEnv(envRoot), ...parseEnv(envLocal) };
|
|
2195
|
+
for (const secret of config.secrets ?? []) {
|
|
2196
|
+
if (merged[secret] !== void 0) continue;
|
|
2197
|
+
if (process.env[secret] !== void 0) {
|
|
2198
|
+
merged[secret] = process.env[secret];
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
throw new Error(
|
|
2202
|
+
`secret "${secret}" \u672A\u914D\u7F6E\uFF1A\u8BF7\u5728 .env.local \u6DFB\u52A0 ${secret}=<value>\uFF0C\u6216\u7528\u73AF\u5883\u53D8\u91CF\u5BFC\u51FA\u540E\u91CD\u8BD5 adep dev\uFF08\u6A21\u62DF\u73AF\u5883\u6C38\u4E0D\u843D\u76D8\u8BE5\u503C\uFF09`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
return merged;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// packages/cli/src/sim/invoke.ts
|
|
2209
|
+
function resolveFunctionEntry(files, fnName) {
|
|
2210
|
+
const flat = `${fnName}.ts`;
|
|
2211
|
+
if (files[flat] !== void 0) return flat;
|
|
2212
|
+
const directory = `${fnName}/index.ts`;
|
|
2213
|
+
if (files[directory] !== void 0) return directory;
|
|
2214
|
+
return void 0;
|
|
2215
|
+
}
|
|
2216
|
+
function createSimInvokeHandler(options) {
|
|
2217
|
+
return async (method, args) => {
|
|
2218
|
+
if (method !== "invoke") {
|
|
2219
|
+
throw new Error(`\u672A\u77E5\u7684 cloud.invoke \u65B9\u6CD5 "${method}"`);
|
|
2220
|
+
}
|
|
2221
|
+
const input = args[0];
|
|
2222
|
+
if (typeof input?.name !== "string" || input.name.length === 0) {
|
|
2223
|
+
throw new Error("cloud.invoke \u9700\u8981\u76EE\u6807\u51FD\u6570\u540D\uFF08InvokeInput.name\uFF09");
|
|
2224
|
+
}
|
|
2225
|
+
const targetEntry = resolveFunctionEntry(options.files, input.name);
|
|
2226
|
+
if (targetEntry === void 0) {
|
|
2227
|
+
throw new Error(`\u76EE\u6807\u51FD\u6570 "${input.name}" \u4E0D\u5B58\u5728`);
|
|
2228
|
+
}
|
|
2229
|
+
const executeInput = {
|
|
2230
|
+
project: { id: "local", slug: "local" },
|
|
2231
|
+
fn: { id: input.name, name: input.name },
|
|
2232
|
+
files: options.files,
|
|
2233
|
+
entry: targetEntry,
|
|
2234
|
+
request: {
|
|
2235
|
+
method: "POST",
|
|
2236
|
+
path: `/${input.name}`,
|
|
2237
|
+
query: input.query ?? {},
|
|
2238
|
+
headers: {},
|
|
2239
|
+
...input.body === void 0 ? {} : { body: input.body }
|
|
2240
|
+
},
|
|
2241
|
+
timeoutMs: 1e4,
|
|
2242
|
+
...options.env === void 0 ? {} : { env: options.env }
|
|
2243
|
+
};
|
|
2244
|
+
const result = await options.executor.execute(executeInput);
|
|
2245
|
+
return result.body;
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// packages/cli/src/sim/boundary.ts
|
|
2250
|
+
var SIM_BOUNDARIES = [
|
|
2251
|
+
{
|
|
2252
|
+
kind: "capability",
|
|
2253
|
+
title: "\u89E6\u53D1\u5668\u4E0D\u81EA\u52A8\u6267\u884C",
|
|
2254
|
+
detail: "\u5B9A\u65F6 / \u4E8B\u4EF6\u89E6\u53D1\u5668\u53EA\u767B\u8BB0\uFF0C\u4E0D\u7531\u6A21\u62DF\u5668\u81EA\u52A8\u89E6\u53D1\uFF08\u672C\u5730\u4EE5\u624B\u5DE5 HTTP \u8C03\u7528\u66FF\u4EE3\uFF09\u3002"
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
kind: "diff",
|
|
2258
|
+
title: "\u65E0\u8DE8\u5B9E\u4F8B\u5E7F\u64AD",
|
|
2259
|
+
detail: "cloud.realtime \u5DF2\u6CE8\u5165\uFF0C\u4F46\u4E3A\u5355\u8FDB\u7A0B\u5185\u5B58\u5E7F\u64AD\uFF1A\u4E0D\u652F\u6301\u8DE8\u8FDB\u7A0B / \u8DE8\u5B9E\u4F8B\uFF0C\u7EBF\u4E0A\u591A\u5B9E\u4F8B\u8BED\u4E49\u4E0D\u540C\u3002"
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
kind: "capability",
|
|
2263
|
+
title: "publish \u4E0D\u652F\u6301 except",
|
|
2264
|
+
detail: "\u672C\u5730\u4E0D\u5EFA WS \u63E1\u624B\u3001\u65E0\u8FDE\u63A5\u8EAB\u4EFD\uFF0C\u6545 cloud.realtime.publish \u7684 except\uFF08\u4E0D\u53D1\u56DE\u53D1\u9001\u8005\uFF09\u65E0\u4ECE\u5B9E\u73B0\uFF0C\u4F20\u5165\u5373\u62A5\u9519\u3002"
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
kind: "capability",
|
|
2268
|
+
title: "\u65E0\u9650\u989D\u7B49\u4EF7",
|
|
2269
|
+
detail: "\u672C\u5730\u4E0D\u6267\u884C\u8BA1\u91CF / \u914D\u989D\u95E8\u7981\uFF08\u4E0D\u6309\u51FD\u6570\u8C03\u7528\u3001\u6D41\u91CF\u3001\u63A8\u9001\u8BA1\u6570\uFF09\uFF0C\u9650\u989D\u8BED\u4E49\u4EC5\u7EBF\u4E0A\u751F\u6548\u3002"
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
kind: "diff",
|
|
2273
|
+
title: "\u4E0D\u8BA1\u91CF",
|
|
2274
|
+
detail: "\u672C\u6A21\u62DF\u8FD0\u884C\u65F6\u4E0D\u4EA7\u51FA\u8BA1\u91CF\u4E8B\u4EF6\uFF0C\u4E5F\u4E0D\u5199 billing\uFF1B\u79BB\u7EBF\u5F00\u53D1\u4E0D\u8BA1\u8D39\u3002"
|
|
2275
|
+
}
|
|
2276
|
+
];
|
|
2277
|
+
|
|
2278
|
+
// packages/cli/src/ts-config.ts
|
|
2279
|
+
import { stat as stat2, readFile as readFile4 } from "node:fs/promises";
|
|
2280
|
+
import { join as join6 } from "node:path";
|
|
2281
|
+
|
|
2282
|
+
// shared/sdk/adep-config.ts
|
|
2283
|
+
var DEFAULT_ADEP_CONFIG = {
|
|
2284
|
+
functionsDir: "functions",
|
|
2285
|
+
functions_prefix: "/api"
|
|
2286
|
+
};
|
|
2287
|
+
function typeNameOf(value) {
|
|
2288
|
+
if (value === null) return "null";
|
|
2289
|
+
if (Array.isArray(value)) return "array";
|
|
2290
|
+
return typeof value;
|
|
2291
|
+
}
|
|
2292
|
+
function asRecord(value) {
|
|
2293
|
+
if (value === null || Array.isArray(value) || typeof value !== "object") return null;
|
|
2294
|
+
return value;
|
|
2295
|
+
}
|
|
2296
|
+
function asStringRecord(value) {
|
|
2297
|
+
const rec = asRecord(value);
|
|
2298
|
+
if (rec === null) return null;
|
|
2299
|
+
for (const v of Object.values(rec)) {
|
|
2300
|
+
if (typeof v !== "string") return null;
|
|
2301
|
+
}
|
|
2302
|
+
return rec;
|
|
2303
|
+
}
|
|
2304
|
+
function parseAdepConfig(raw) {
|
|
2305
|
+
if (raw === null || raw === void 0) {
|
|
2306
|
+
return { ok: true, data: { ...DEFAULT_ADEP_CONFIG } };
|
|
2307
|
+
}
|
|
2308
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
2309
|
+
return {
|
|
2310
|
+
ok: false,
|
|
2311
|
+
errors: [`adep.config.ts \u7684 default export \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u6536\u5230 ${typeNameOf(raw)}`]
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
const errors = [];
|
|
2315
|
+
const input = raw;
|
|
2316
|
+
const data = { ...DEFAULT_ADEP_CONFIG };
|
|
2317
|
+
if (input.name !== void 0) {
|
|
2318
|
+
if (typeof input.name !== "string") {
|
|
2319
|
+
errors.push(`name \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(input.name)}`);
|
|
2320
|
+
} else {
|
|
2321
|
+
data.name = input.name;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
if (input.functionsDir !== void 0) {
|
|
2325
|
+
if (typeof input.functionsDir !== "string") {
|
|
2326
|
+
errors.push(`functionsDir \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(input.functionsDir)}`);
|
|
2327
|
+
} else {
|
|
2328
|
+
data.functionsDir = input.functionsDir;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (input.functions_prefix !== void 0) {
|
|
2332
|
+
if (typeof input.functions_prefix !== "string") {
|
|
2333
|
+
errors.push(`functions_prefix \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF0C\u6536\u5230 ${typeNameOf(input.functions_prefix)}`);
|
|
2334
|
+
} else {
|
|
2335
|
+
data.functions_prefix = input.functions_prefix;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (input.frontend !== void 0) {
|
|
2339
|
+
const rec = asRecord(input.frontend);
|
|
2340
|
+
if (rec === null) {
|
|
2341
|
+
errors.push(`frontend \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u6536\u5230 ${typeNameOf(input.frontend)}`);
|
|
2342
|
+
} else {
|
|
2343
|
+
data.frontend = rec;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
if (input.environment !== void 0) {
|
|
2347
|
+
const rec = asStringRecord(input.environment);
|
|
2348
|
+
if (rec === null) {
|
|
2349
|
+
errors.push(
|
|
2350
|
+
`environment \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u5230\u5B57\u7B26\u4E32\u7684\u6620\u5C04\uFF08Record<string, string>\uFF09\uFF0C\u6536\u5230 ${typeNameOf(input.environment)}`
|
|
2351
|
+
);
|
|
2352
|
+
} else {
|
|
2353
|
+
data.environment = rec;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
if (input.runtime !== void 0) {
|
|
2357
|
+
const rec = asRecord(input.runtime);
|
|
2358
|
+
if (rec === null) {
|
|
2359
|
+
errors.push(`runtime \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u6536\u5230 ${typeNameOf(input.runtime)}`);
|
|
2360
|
+
} else {
|
|
2361
|
+
data.runtime = rec;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
2365
|
+
return { ok: true, data };
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// packages/cli/src/ts-config.ts
|
|
2369
|
+
async function loadAdepConfigModule(cwd) {
|
|
2370
|
+
const configPath = join6(cwd, "adep.config.ts");
|
|
2371
|
+
try {
|
|
2372
|
+
await stat2(configPath);
|
|
2373
|
+
} catch {
|
|
2374
|
+
return null;
|
|
2375
|
+
}
|
|
2376
|
+
let rawDefault;
|
|
2377
|
+
try {
|
|
2378
|
+
const bun = globalThis.Bun;
|
|
2379
|
+
if (bun !== void 0) {
|
|
2380
|
+
const mod = await import(configPath);
|
|
2381
|
+
rawDefault = mod.default;
|
|
2382
|
+
} else {
|
|
2383
|
+
const { transform } = await import("esbuild");
|
|
2384
|
+
const source = await readFile4(configPath, "utf8");
|
|
2385
|
+
const { code } = await transform(source, {
|
|
2386
|
+
loader: "ts",
|
|
2387
|
+
format: "esm",
|
|
2388
|
+
target: "node22"
|
|
2389
|
+
});
|
|
2390
|
+
const url = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
|
|
2391
|
+
const mod = await import(url);
|
|
2392
|
+
rawDefault = mod.default;
|
|
2393
|
+
}
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2396
|
+
const message = `adep.config.ts \u52A0\u8F7D\u5931\u8D25\uFF1A${reason}`;
|
|
2397
|
+
process.stderr.write(`\u9519\u8BEF\uFF1A${message}
|
|
2398
|
+
`);
|
|
2399
|
+
throw new Error(message, { cause: error });
|
|
2400
|
+
}
|
|
2401
|
+
const result = parseAdepConfig(rawDefault);
|
|
2402
|
+
if (!result.ok) {
|
|
2403
|
+
const message = `adep.config.ts \u6821\u9A8C\u5931\u8D25\uFF1A
|
|
2404
|
+
${result.errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
2405
|
+
process.stderr.write(`${message}
|
|
2406
|
+
`);
|
|
2407
|
+
throw new Error(message);
|
|
2408
|
+
}
|
|
2409
|
+
return result.data;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// packages/cli/src/dev.ts
|
|
2413
|
+
async function loadConfig(cwd) {
|
|
2414
|
+
const name = basename(resolve2(cwd));
|
|
2415
|
+
const config = await loadAdepConfigModule(cwd);
|
|
2416
|
+
return {
|
|
2417
|
+
name: config?.name ?? name,
|
|
2418
|
+
functionsDir: config?.functionsDir ?? "functions",
|
|
2419
|
+
functionsPrefix: config?.functions_prefix ?? ""
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
function parseEnvFile(content) {
|
|
2423
|
+
const env = {};
|
|
2424
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
2425
|
+
const line = raw.trim();
|
|
2426
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
2427
|
+
const eq = line.indexOf("=");
|
|
2428
|
+
if (eq === -1) continue;
|
|
2429
|
+
const key = line.slice(0, eq).trim();
|
|
2430
|
+
if (key.length === 0) continue;
|
|
2431
|
+
env[key] = line.slice(eq + 1).trim();
|
|
2432
|
+
}
|
|
2433
|
+
return env;
|
|
2434
|
+
}
|
|
2435
|
+
async function collectFunctions(dir) {
|
|
2436
|
+
const files = {};
|
|
2437
|
+
const walk = async (sub, prefix) => {
|
|
2438
|
+
let entries;
|
|
2439
|
+
try {
|
|
2440
|
+
entries = await readdir2(sub, { withFileTypes: true });
|
|
2441
|
+
} catch {
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
for (const entry of entries) {
|
|
2445
|
+
const rel = prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
2446
|
+
const full = join7(sub, entry.name);
|
|
2447
|
+
if (entry.isDirectory()) {
|
|
2448
|
+
await walk(full, rel);
|
|
2449
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
2450
|
+
files[rel] = await readFile5(full, "utf8");
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
await walk(dir, "");
|
|
2455
|
+
return files;
|
|
2456
|
+
}
|
|
2457
|
+
function bodyOf(req) {
|
|
2458
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
2459
|
+
const chunks = [];
|
|
2460
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
2461
|
+
req.on("end", () => {
|
|
2462
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
2463
|
+
if (raw.length === 0) {
|
|
2464
|
+
resolveBody(void 0);
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
try {
|
|
2468
|
+
resolveBody(JSON.parse(raw));
|
|
2469
|
+
} catch {
|
|
2470
|
+
resolveBody(raw);
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
req.on("error", rejectBody);
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
function resolveRouteFnName(pathname, prefix) {
|
|
2477
|
+
const segments = pathname.split("/").filter((segment) => segment.length > 0);
|
|
2478
|
+
const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, "");
|
|
2479
|
+
if (normalizedPrefix.length === 0) {
|
|
2480
|
+
const first = segments[0];
|
|
2481
|
+
return first === void 0 ? {} : { fnName: first };
|
|
2482
|
+
}
|
|
2483
|
+
if (segments[0] !== normalizedPrefix) {
|
|
2484
|
+
return {
|
|
2485
|
+
error: {
|
|
2486
|
+
code: "FN_PREFIX_REQUIRED",
|
|
2487
|
+
message: `\u5DF2\u914D\u7F6E functions_prefix "${normalizedPrefix}"\uFF1A\u4EE5 /${normalizedPrefix}/{fnName} \u8C03\u7528\u672C\u5730\u51FD\u6570`
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
const second = segments[1];
|
|
2492
|
+
if (second === void 0) {
|
|
2493
|
+
return {
|
|
2494
|
+
error: {
|
|
2495
|
+
code: "FN_NAME_REQUIRED",
|
|
2496
|
+
message: `\u4EE5 /${normalizedPrefix}/{fnName} \u8C03\u7528\u672C\u5730\u51FD\u6570`
|
|
2497
|
+
}
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
return { fnName: second };
|
|
2501
|
+
}
|
|
2502
|
+
function writeJson(res, status, payload) {
|
|
2503
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
2504
|
+
res.end(JSON.stringify(payload));
|
|
2505
|
+
}
|
|
2506
|
+
function printBoundaries(log) {
|
|
2507
|
+
log("[adep] \u80FD\u529B\u8FB9\u754C\uFF08\u6A21\u62DF\u8FD0\u884C\u65F6\u53EA\u66FF\u6362\u4F20\u8F93/\u6301\u4E45\u5316\uFF0C\u4E0D\u6539\u5199\u8BED\u4E49\uFF09\uFF1A");
|
|
2508
|
+
for (const b of SIM_BOUNDARIES) {
|
|
2509
|
+
log(`[adep] - [${b.kind}] ${b.title}\uFF1A${b.detail}`);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
async function ensureGitignore(cwd) {
|
|
2513
|
+
const gitignorePath = join7(cwd, ".gitignore");
|
|
2514
|
+
const existing = await readFile5(gitignorePath, "utf8").catch(() => "");
|
|
2515
|
+
if (existing.split(/\r?\n/).includes(".adep/")) return;
|
|
2516
|
+
await writeFile3(gitignorePath, `${existing.replace(/\n+$/, "")}
|
|
2517
|
+
.adep/
|
|
2518
|
+
`, "utf8");
|
|
2519
|
+
}
|
|
2520
|
+
async function startDevServer(options) {
|
|
2521
|
+
const cwd = resolve2(options.cwd);
|
|
2522
|
+
const log = options.log ?? ((line) => process.stdout.write(`${line}
|
|
2523
|
+
`));
|
|
2524
|
+
const config = await loadConfig(cwd);
|
|
2525
|
+
if (options.prefix !== void 0) {
|
|
2526
|
+
config.functionsPrefix = options.prefix.replace(/^\/+|\/+$/g, "");
|
|
2527
|
+
}
|
|
2528
|
+
const functionsDir = join7(cwd, config.functionsDir);
|
|
2529
|
+
const coldStartAt = Date.now();
|
|
2530
|
+
const executor = new WorkerFunctionExecutor();
|
|
2531
|
+
await mkdir3(functionsDir, { recursive: true });
|
|
2532
|
+
const runtime = await createSimRuntime({
|
|
2533
|
+
cwd,
|
|
2534
|
+
projectId: "local",
|
|
2535
|
+
baseUrl: `http://127.0.0.1:${options.port ?? 8787}`,
|
|
2536
|
+
log
|
|
2537
|
+
});
|
|
2538
|
+
await ensureGitignore(cwd);
|
|
2539
|
+
let files = await collectFunctions(functionsDir);
|
|
2540
|
+
let env = await loadSimEnv(cwd).catch(() => ({}));
|
|
2541
|
+
const dbBundle = runtime.bundle;
|
|
2542
|
+
const invokeHandler = createSimInvokeHandler({ executor, files });
|
|
2543
|
+
const reload = async () => {
|
|
2544
|
+
const [nextFiles, envText2] = await Promise.all([
|
|
2545
|
+
collectFunctions(functionsDir),
|
|
2546
|
+
readFile5(join7(cwd, ".env.local"), "utf8").catch(() => "")
|
|
2547
|
+
]);
|
|
2548
|
+
files = nextFiles;
|
|
2549
|
+
env = parseEnvFile(envText2);
|
|
2550
|
+
};
|
|
2551
|
+
const envText = await readFile5(join7(cwd, ".env.local"), "utf8").catch(() => "");
|
|
2552
|
+
env = parseEnvFile(envText);
|
|
2553
|
+
let debounceTimer;
|
|
2554
|
+
let watcher;
|
|
2555
|
+
try {
|
|
2556
|
+
watcher = watch(cwd, { recursive: true }, () => {
|
|
2557
|
+
if (debounceTimer !== void 0) clearTimeout(debounceTimer);
|
|
2558
|
+
debounceTimer = setTimeout(() => {
|
|
2559
|
+
void reload();
|
|
2560
|
+
}, 120);
|
|
2561
|
+
});
|
|
2562
|
+
} catch {
|
|
2563
|
+
watcher = void 0;
|
|
2564
|
+
}
|
|
2565
|
+
const buildInput = async (fnName, entry, url, req) => {
|
|
2566
|
+
const query = {};
|
|
2567
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
2568
|
+
const values = url.searchParams.getAll(key);
|
|
2569
|
+
query[key] = values.length > 1 ? values.join(",") : values[0] ?? "";
|
|
2570
|
+
}
|
|
2571
|
+
const headers = {};
|
|
2572
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
2573
|
+
if (typeof value === "string") headers[key] = value;
|
|
2574
|
+
}
|
|
2575
|
+
const body = req.method === "GET" || req.method === "HEAD" ? void 0 : await bodyOf(req);
|
|
2576
|
+
const hasEnv = Object.keys(env).length > 0;
|
|
2577
|
+
return {
|
|
2578
|
+
project: { id: "local", slug: config.name },
|
|
2579
|
+
fn: { id: fnName, name: fnName },
|
|
2580
|
+
files,
|
|
2581
|
+
entry,
|
|
2582
|
+
request: {
|
|
2583
|
+
method: req.method ?? "GET",
|
|
2584
|
+
path: url.pathname,
|
|
2585
|
+
query,
|
|
2586
|
+
headers,
|
|
2587
|
+
...body === void 0 ? {} : { body }
|
|
2588
|
+
},
|
|
2589
|
+
// 模拟运行时能力:db / storage / realtime 各自只读复用线上装配,语义一致。
|
|
2590
|
+
capabilities: dbBundle.capabilities,
|
|
2591
|
+
rpcHandlers: {
|
|
2592
|
+
...dbBundle.rpcHandlers,
|
|
2593
|
+
invoke: invokeHandler
|
|
2594
|
+
},
|
|
2595
|
+
timeoutMs: 1e4,
|
|
2596
|
+
...hasEnv ? { env } : {}
|
|
2597
|
+
};
|
|
2598
|
+
};
|
|
2599
|
+
const handle = async (req, res) => {
|
|
2600
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2601
|
+
const { fnName, error: routeError } = resolveRouteFnName(url.pathname, config.functionsPrefix);
|
|
2602
|
+
if (routeError !== void 0) {
|
|
2603
|
+
writeJson(res, 400, { error: routeError });
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
if (fnName === void 0) {
|
|
2607
|
+
writeJson(res, 400, {
|
|
2608
|
+
error: { code: "FN_NAME_REQUIRED", message: "\u4EE5 /{fnName} \u8C03\u7528\u672C\u5730\u51FD\u6570" }
|
|
2609
|
+
});
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const entry = resolveFunctionEntry(files, fnName);
|
|
2613
|
+
if (entry === void 0) {
|
|
2614
|
+
log(`[adep] ${req.method ?? "GET"} /${fnName} -> 404\uFF08\u51FD\u6570\u4E0D\u5B58\u5728\uFF09`);
|
|
2615
|
+
writeJson(res, 404, { error: { code: "FN_NOT_FOUND", message: `\u51FD\u6570 "${fnName}" \u4E0D\u5B58\u5728` } });
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const input = await buildInput(fnName, entry, url, req);
|
|
2619
|
+
const startedAt = performance.now();
|
|
2620
|
+
try {
|
|
2621
|
+
const result = await executor.execute(input);
|
|
2622
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
2623
|
+
log(`[adep] ${req.method ?? "GET"} /${fnName} -> 200 ${durationMs}ms`);
|
|
2624
|
+
for (const line of result.logs) log(`[adep] ${line}`);
|
|
2625
|
+
writeJson(res, 200, result.body === void 0 ? null : result.body);
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
2628
|
+
if (error instanceof ExecutorError) {
|
|
2629
|
+
log(
|
|
2630
|
+
`[adep] ${req.method ?? "GET"} /${fnName} -> ${error.status} ${durationMs}ms (${error.code})`
|
|
2631
|
+
);
|
|
2632
|
+
writeJson(res, error.status, { error: { code: error.code, message: error.message } });
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
log(`[adep] ${req.method ?? "GET"} /${fnName} -> 500 ${durationMs}ms`);
|
|
2636
|
+
writeJson(res, 500, {
|
|
2637
|
+
error: {
|
|
2638
|
+
code: "FN_EXEC_ERROR",
|
|
2639
|
+
message: error instanceof Error ? error.message : "\u51FD\u6570\u6267\u884C\u5931\u8D25"
|
|
2640
|
+
}
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
const server = createServer((req, res) => {
|
|
2645
|
+
void handle(req, res);
|
|
2646
|
+
});
|
|
2647
|
+
const port = options.port ?? 8787;
|
|
2648
|
+
const disposeAfterListenError = async () => {
|
|
2649
|
+
if (debounceTimer !== void 0) clearTimeout(debounceTimer);
|
|
2650
|
+
try {
|
|
2651
|
+
watcher?.close();
|
|
2652
|
+
} catch {
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
await executor.dispose();
|
|
2656
|
+
} catch {
|
|
2657
|
+
}
|
|
2658
|
+
try {
|
|
2659
|
+
await runtime.dispose();
|
|
2660
|
+
} catch {
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
2664
|
+
const onListenError = (error) => {
|
|
2665
|
+
void disposeAfterListenError().finally(() => rejectListen(error));
|
|
2666
|
+
};
|
|
2667
|
+
server.once("error", onListenError);
|
|
2668
|
+
server.listen(port, "127.0.0.1", () => {
|
|
2669
|
+
server.off("error", onListenError);
|
|
2670
|
+
resolveListen();
|
|
2671
|
+
});
|
|
2672
|
+
});
|
|
2673
|
+
const address = server.address();
|
|
2674
|
+
const actualPort = typeof address === "object" && address !== null ? address.port : port;
|
|
2675
|
+
const baseUrl = `http://127.0.0.1:${actualPort}`;
|
|
2676
|
+
const coldStartMs = Date.now() - coldStartAt;
|
|
2677
|
+
const curlPath = config.functionsPrefix.length === 0 ? "/hello" : `/${config.functionsPrefix}/hello`;
|
|
2678
|
+
log(`[adep] dev server listening on ${baseUrl}\uFF08\u6A21\u62DF\u8FD0\u884C\u65F6\uFF0C\u51B7\u542F\u52A8 ${coldStartMs}ms\uFF09`);
|
|
2679
|
+
log(`[adep] curl \u793A\u4F8B\uFF1Acurl ${baseUrl}${curlPath}`);
|
|
2680
|
+
printBoundaries(log);
|
|
2681
|
+
log(`[adep] \u6A21\u62DF\u6570\u636E\u76EE\u5F55\uFF1A${simDir(cwd)}`);
|
|
2682
|
+
return {
|
|
2683
|
+
port: actualPort,
|
|
2684
|
+
baseUrl,
|
|
2685
|
+
close: async () => {
|
|
2686
|
+
if (debounceTimer !== void 0) clearTimeout(debounceTimer);
|
|
2687
|
+
watcher?.close();
|
|
2688
|
+
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
2689
|
+
await executor.dispose();
|
|
2690
|
+
await runtime.dispose();
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// packages/cli/src/vite.ts
|
|
2696
|
+
import {
|
|
2697
|
+
createAdepFnProxyScriptMiddleware,
|
|
2698
|
+
createAdepProxyMiddleware,
|
|
2699
|
+
FN_FETCH_INTERCEPTOR_SCRIPT,
|
|
2700
|
+
injectFnProxyScriptTag,
|
|
2701
|
+
isAddrInUse,
|
|
2702
|
+
normalizeFunctionPrefix
|
|
2703
|
+
} from "@adep/vite-plugin";
|
|
2704
|
+
var createDevServer = (opts) => startDevServer({ cwd: opts.cwd, port: opts.port, prefix: opts.prefix, log: opts.log });
|
|
2705
|
+
var loadConfig2 = (cwd) => loadAdepConfigModule(cwd);
|
|
2706
|
+
function adepVitePlugin(options = {}) {
|
|
2707
|
+
return adepPlugin({ ...options, createDevServer, loadConfig: loadConfig2 });
|
|
2708
|
+
}
|
|
2709
|
+
var vite_default = adepVitePlugin;
|
|
2710
|
+
export {
|
|
2711
|
+
FN_FETCH_INTERCEPTOR_SCRIPT,
|
|
2712
|
+
adepVitePlugin,
|
|
2713
|
+
createAdepFnProxyScriptMiddleware,
|
|
2714
|
+
createAdepProxyMiddleware,
|
|
2715
|
+
vite_default as default,
|
|
2716
|
+
injectFnProxyScriptTag,
|
|
2717
|
+
isAddrInUse,
|
|
2718
|
+
normalizeFunctionPrefix
|
|
2719
|
+
};
|