naro-mcp 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -16
- package/dist/index.js +1091 -128
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -48,6 +48,12 @@ async function errorFromResponse(response, context) {
|
|
|
48
48
|
const serverMessage = typeof body?.error?.message === "string" ? body.error.message : null;
|
|
49
49
|
const code = serverCode ?? (response.status === 401 ? "UNAUTHORIZED" : response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR");
|
|
50
50
|
const details = {};
|
|
51
|
+
const serverDetails = body?.error?.details;
|
|
52
|
+
if (typeof serverDetails === "object" && serverDetails !== null && !Array.isArray(serverDetails))
|
|
53
|
+
Object.assign(details, serverDetails);
|
|
54
|
+
const retryAfter = response.headers.get("retry-after")?.trim() ?? "";
|
|
55
|
+
if (response.status === 429 && /^\d{1,9}$/.test(retryAfter))
|
|
56
|
+
details.retryAfter = Number(retryAfter);
|
|
51
57
|
if (body?.error?.fields !== void 0) details.fields = body.error.fields;
|
|
52
58
|
if (typeof body?.error?.installUrl === "string")
|
|
53
59
|
details.installUrl = body.error.installUrl;
|
|
@@ -459,6 +465,18 @@ var TERMINAL_DEPLOYMENT_STATUSES = [
|
|
|
459
465
|
function isTerminalStatus(status) {
|
|
460
466
|
return TERMINAL_DEPLOYMENT_STATUSES.includes(status);
|
|
461
467
|
}
|
|
468
|
+
var SCHEMA_COLUMN_TYPES = [
|
|
469
|
+
"INTEGER",
|
|
470
|
+
"REAL",
|
|
471
|
+
"TEXT",
|
|
472
|
+
"BLOB",
|
|
473
|
+
"NUMERIC",
|
|
474
|
+
"BOOLEAN",
|
|
475
|
+
"DATETIME",
|
|
476
|
+
"JSON"
|
|
477
|
+
];
|
|
478
|
+
var STALLED_GRACE_MS = 2 * 6e4;
|
|
479
|
+
var POLICY_ACTIONS = ["select", "insert", "update", "delete"];
|
|
462
480
|
function projectInfo(row) {
|
|
463
481
|
return {
|
|
464
482
|
id: row.id,
|
|
@@ -516,6 +534,7 @@ var NaroClient = class {
|
|
|
516
534
|
domains;
|
|
517
535
|
logs;
|
|
518
536
|
db;
|
|
537
|
+
backend;
|
|
519
538
|
constructor(options2) {
|
|
520
539
|
this.baseUrl = (options2.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
|
|
521
540
|
this.getAccessToken = options2.getAccessToken;
|
|
@@ -529,9 +548,11 @@ var NaroClient = class {
|
|
|
529
548
|
this.domains = new DomainsApi(this);
|
|
530
549
|
this.logs = new LogsApi(this);
|
|
531
550
|
this.db = new DbApi(this);
|
|
551
|
+
this.backend = new BackendApi(this);
|
|
532
552
|
}
|
|
533
553
|
async request(path, options2) {
|
|
534
554
|
const token = await this.getAccessToken();
|
|
555
|
+
const timeoutMs = options2.timeoutMs ?? this.requestTimeoutMs;
|
|
535
556
|
let response;
|
|
536
557
|
try {
|
|
537
558
|
response = await this.doFetch(`${this.baseUrl}${path}`, {
|
|
@@ -541,13 +562,13 @@ var NaroClient = class {
|
|
|
541
562
|
...options2.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
542
563
|
},
|
|
543
564
|
...options2.body !== void 0 ? { body: JSON.stringify(options2.body) } : {},
|
|
544
|
-
signal: AbortSignal.timeout(
|
|
565
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
545
566
|
});
|
|
546
567
|
} catch (error51) {
|
|
547
568
|
if (error51 instanceof Error && (error51.name === "TimeoutError" || error51.name === "AbortError")) {
|
|
548
569
|
throw new NaroError(
|
|
549
570
|
"TIMEOUT",
|
|
550
|
-
`${options2.context} timed out after ${String(Math.round(
|
|
571
|
+
`${options2.context} timed out after ${String(Math.round(timeoutMs / 1e3))}s.`,
|
|
551
572
|
{ cause: error51 }
|
|
552
573
|
);
|
|
553
574
|
}
|
|
@@ -1115,6 +1136,255 @@ var DbApi = class {
|
|
|
1115
1136
|
context: "Pull schema"
|
|
1116
1137
|
});
|
|
1117
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Changes the schema without SQL (spec 2026-09-24 P2-4): the server
|
|
1141
|
+
* renders the DDL, checks it and applies it as ONE migration recorded in
|
|
1142
|
+
* `d1_migrations` — all operations or none. `dryRun` returns the SQL and
|
|
1143
|
+
* the name without applying anything. Save `sql` as `migrations/<name>` so
|
|
1144
|
+
* `naro db reset` re-applies it; a refused operation's index is in
|
|
1145
|
+
* `details.operation`.
|
|
1146
|
+
*/
|
|
1147
|
+
async schema(projectId, operations, options2 = {}) {
|
|
1148
|
+
return this.client.request(
|
|
1149
|
+
this.path(projectId, "/schema"),
|
|
1150
|
+
{
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
// Only the keys given: the server validates each one it sees.
|
|
1153
|
+
body: {
|
|
1154
|
+
operations,
|
|
1155
|
+
...options2.dryRun !== void 0 ? { dryRun: options2.dryRun } : {},
|
|
1156
|
+
...options2.name !== void 0 ? { name: options2.name } : {}
|
|
1157
|
+
},
|
|
1158
|
+
context: "Change schema"
|
|
1159
|
+
}
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
var PROVISION_TIMEOUT_MS = 12e4;
|
|
1164
|
+
function backendInfo(body) {
|
|
1165
|
+
return {
|
|
1166
|
+
status: body.status,
|
|
1167
|
+
step: body.step,
|
|
1168
|
+
error: body.error,
|
|
1169
|
+
url: body.url,
|
|
1170
|
+
anonKey: body.anonKey,
|
|
1171
|
+
serviceKeyPrefix: body.serviceKeyPrefix,
|
|
1172
|
+
leaseExpiresAt: body.leaseExpiresAt ?? null,
|
|
1173
|
+
// Only a real verdict: without it `isStalledBackend` judges locally.
|
|
1174
|
+
...typeof body.stalled === "boolean" ? { stalled: body.stalled } : {},
|
|
1175
|
+
// A control plane from before the toggle could not turn a backend off:
|
|
1176
|
+
// whatever it has is on.
|
|
1177
|
+
enabled: typeof body.enabled === "boolean" ? body.enabled : body.status !== "none",
|
|
1178
|
+
disabledAt: body.disabledAt ?? null,
|
|
1179
|
+
turningOff: body.turningOff === true
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
var BackendApi = class {
|
|
1183
|
+
constructor(client) {
|
|
1184
|
+
this.client = client;
|
|
1185
|
+
}
|
|
1186
|
+
path(projectId, suffix = "") {
|
|
1187
|
+
return `/api/projects/${encodeURIComponent(projectId)}/backend${suffix}`;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* `status: "none"` until the backend is first turned on; `enabled` says
|
|
1191
|
+
* whether it is on now.
|
|
1192
|
+
*/
|
|
1193
|
+
async info(projectId) {
|
|
1194
|
+
const body = await this.client.request(this.path(projectId), {
|
|
1195
|
+
context: "Read backend"
|
|
1196
|
+
});
|
|
1197
|
+
return backendInfo(body);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Turns the backend on (toggle spec §2.3): the first time it provisions
|
|
1201
|
+
* the D1, the keys and the Naro worker; a backend that was turned off
|
|
1202
|
+
* comes back with the same keys — its worker is uploaded again, nothing
|
|
1203
|
+
* else is made anew. Safe to repeat, and a repair: a ready backend that
|
|
1204
|
+
* is on has its routes written again and its worker checked (no upload),
|
|
1205
|
+
* a failed run resumes at its step. A step that fails is not an
|
|
1206
|
+
* HTTP error — the answer is 200 with `status: "error"`, `step` and
|
|
1207
|
+
* `error`. While another run holds the project's lease this answers the
|
|
1208
|
+
* row as it stands (`provisioning`).
|
|
1209
|
+
*/
|
|
1210
|
+
async enable(projectId) {
|
|
1211
|
+
const body = await this.client.request(
|
|
1212
|
+
this.path(projectId, "/enable"),
|
|
1213
|
+
{
|
|
1214
|
+
method: "POST",
|
|
1215
|
+
context: "Enable backend",
|
|
1216
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
1217
|
+
}
|
|
1218
|
+
);
|
|
1219
|
+
return backendInfo(body);
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Turns the backend off (toggle spec §2.2): the dispatcher's routes to
|
|
1223
|
+
* the Naro worker go, so the app's sign-ins and data requests answer 404
|
|
1224
|
+
* from `/_naro/*` — in up to about 90 seconds, as the dispatcher's cache
|
|
1225
|
+
* ages out. The D1 with its data and users, the keys, the sign-in
|
|
1226
|
+
* settings and the worker stay; `enable` brings it back as it was.
|
|
1227
|
+
* Safe to repeat, and a repair: a backend already off has its routes
|
|
1228
|
+
* removed again — what finishes a turn-off that stopped half-way
|
|
1229
|
+
* (`turningOff`) — and keeps its first `disabledAt`. No backend at all
|
|
1230
|
+
* answers as it stands. `BACKEND_BUSY` while another run holds the lease.
|
|
1231
|
+
*/
|
|
1232
|
+
async disable(projectId) {
|
|
1233
|
+
const body = await this.client.request(
|
|
1234
|
+
this.path(projectId, "/disable"),
|
|
1235
|
+
{ method: "POST", context: "Disable backend" }
|
|
1236
|
+
);
|
|
1237
|
+
return backendInfo(body);
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* `enable` under its first name: the control plane keeps
|
|
1241
|
+
* `POST …/backend/provision` as the same call.
|
|
1242
|
+
*/
|
|
1243
|
+
async provision(projectId) {
|
|
1244
|
+
const body = await this.client.request(
|
|
1245
|
+
this.path(projectId, "/provision"),
|
|
1246
|
+
{
|
|
1247
|
+
method: "POST",
|
|
1248
|
+
context: "Provision backend",
|
|
1249
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
1250
|
+
}
|
|
1251
|
+
);
|
|
1252
|
+
return backendInfo(body);
|
|
1253
|
+
}
|
|
1254
|
+
/** The anon key and the service key's prefix; BACKEND_NOT_READY before provisioning. */
|
|
1255
|
+
async keys(projectId) {
|
|
1256
|
+
const body = await this.client.request(
|
|
1257
|
+
this.path(projectId, "/keys"),
|
|
1258
|
+
{ context: "Read API keys" }
|
|
1259
|
+
);
|
|
1260
|
+
return { anonKey: body.anonKey, serviceKeyPrefix: body.serviceKeyPrefix };
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* The whole service key. It skips every policy — servers only. A role
|
|
1264
|
+
* without `database.write` gets `FORBIDDEN` "Your role cannot reveal the
|
|
1265
|
+
* service key."
|
|
1266
|
+
*/
|
|
1267
|
+
async revealServiceKey(projectId) {
|
|
1268
|
+
const body = await this.client.request(
|
|
1269
|
+
this.path(projectId, "/keys?reveal=service"),
|
|
1270
|
+
{ context: "Reveal service key" }
|
|
1271
|
+
);
|
|
1272
|
+
return body.serviceKey;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Replaces the live key of one kind, in two phases (spec §6 "(P1-3 리뷰
|
|
1276
|
+
* 반영)" I3): the worker takes the new hash first, and only then does the
|
|
1277
|
+
* database swap keys. There is no overlap — the old key stops working once
|
|
1278
|
+
* both are done. Only a `ready` backend that is on rotates
|
|
1279
|
+
* (`BACKEND_NOT_READY`, `BACKEND_BUSY`, or `BACKEND_OFF` while it is
|
|
1280
|
+
* turned off).
|
|
1281
|
+
*
|
|
1282
|
+
* The database decides the outcome. Whenever the new key ends up live —
|
|
1283
|
+
* even if a step after the swap failed — this resolves with the key and
|
|
1284
|
+
* `backend.status` `error`. Otherwise it throws `BACKEND_ROTATION_FAILED`
|
|
1285
|
+
* with no key; that usually leaves the old key working, but a 5xx, a
|
|
1286
|
+
* timeout or a lost connection is not proof either way: `keys()` (and
|
|
1287
|
+
* `revealServiceKey()`) show which key is live, and `enable` puts the
|
|
1288
|
+
* stored keys back on the worker. For a few seconds during any rotation
|
|
1289
|
+
* the worker can already refuse the old key.
|
|
1290
|
+
*/
|
|
1291
|
+
async rotateKey(projectId, kind) {
|
|
1292
|
+
const body = await this.client.request(this.path(projectId, "/keys/rotate"), {
|
|
1293
|
+
method: "POST",
|
|
1294
|
+
body: { kind },
|
|
1295
|
+
context: "Rotate API key",
|
|
1296
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
1297
|
+
});
|
|
1298
|
+
const backend = backendInfo(body);
|
|
1299
|
+
const key = kind === "anon" ? backend.anonKey : body.serviceKey;
|
|
1300
|
+
if (typeof key !== "string" || key === "")
|
|
1301
|
+
throw new NaroError(
|
|
1302
|
+
"INTERNAL_ERROR",
|
|
1303
|
+
"The rotation answer carried no new key. `naro backend keys` shows the key stored now (--reveal for the service key).",
|
|
1304
|
+
{ details: { backend } }
|
|
1305
|
+
);
|
|
1306
|
+
return { backend, key };
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* The sign-in settings (spec P2-6 §12): email, Google and GitHub, the
|
|
1310
|
+
* allowed redirect origins, whether sign-in waits for a confirmed email,
|
|
1311
|
+
* and the callback URLs to register with the providers. Never a secret.
|
|
1312
|
+
*/
|
|
1313
|
+
async auth(projectId) {
|
|
1314
|
+
return this.client.request(
|
|
1315
|
+
this.path(projectId, "/auth"),
|
|
1316
|
+
{
|
|
1317
|
+
context: "Read sign-in settings"
|
|
1318
|
+
}
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Changes the given settings and uploads the Naro worker again with them,
|
|
1323
|
+
* under the provisioning lease: `BACKEND_BUSY` while another run holds it
|
|
1324
|
+
* (nothing is saved), `BACKEND_NOT_READY` before the first provision,
|
|
1325
|
+
* `BACKEND_OFF` while the backend is turned off (nothing is saved),
|
|
1326
|
+
* `VERIFICATION_NEEDS_EMAIL` for a state where nobody could sign in. A
|
|
1327
|
+
* failed upload is not an error — `backend.status` says `error` and the
|
|
1328
|
+
* settings are saved; `enable` finishes. Two minutes, like enable.
|
|
1329
|
+
*/
|
|
1330
|
+
async setAuth(projectId, patch) {
|
|
1331
|
+
const answer = await this.client.request(
|
|
1332
|
+
this.path(projectId, "/auth"),
|
|
1333
|
+
{
|
|
1334
|
+
method: "PUT",
|
|
1335
|
+
body: patch,
|
|
1336
|
+
context: "Change sign-in settings",
|
|
1337
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
1338
|
+
}
|
|
1339
|
+
);
|
|
1340
|
+
return { auth: answer.auth, backend: backendInfo(answer.backend) };
|
|
1341
|
+
}
|
|
1342
|
+
/** Removes email or one provider; the same answer as `setAuth`. */
|
|
1343
|
+
async removeAuth(projectId, target) {
|
|
1344
|
+
const answer = await this.client.request(
|
|
1345
|
+
this.path(projectId, `/auth?target=${encodeURIComponent(target)}`),
|
|
1346
|
+
{
|
|
1347
|
+
method: "DELETE",
|
|
1348
|
+
context: "Change sign-in settings",
|
|
1349
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
return { auth: answer.auth, backend: backendInfo(answer.backend) };
|
|
1353
|
+
}
|
|
1354
|
+
/** Every stored policy, by table name. Tables without one deny anon and signed-in requests. */
|
|
1355
|
+
async policies(projectId) {
|
|
1356
|
+
const body = await this.client.request(
|
|
1357
|
+
this.path(projectId, "/policies"),
|
|
1358
|
+
{ context: "List policies" }
|
|
1359
|
+
);
|
|
1360
|
+
return body.policies;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Merges the given fields into the table's policy (what is left out keeps
|
|
1364
|
+
* its stored value, or `none`) and returns the whole row as stored, under
|
|
1365
|
+
* the table's own spelling. An `owner` rule needs `ownerColumn`.
|
|
1366
|
+
*/
|
|
1367
|
+
async setPolicy(projectId, table, patch) {
|
|
1368
|
+
const body = {};
|
|
1369
|
+
for (const key of [...POLICY_ACTIONS, "ownerColumn"])
|
|
1370
|
+
if (patch[key] !== void 0) body[key] = patch[key];
|
|
1371
|
+
const answer = await this.client.request(
|
|
1372
|
+
this.path(projectId, `/policies/${encodeURIComponent(table)}`),
|
|
1373
|
+
{ method: "PUT", body, context: "Set policy" }
|
|
1374
|
+
);
|
|
1375
|
+
return answer.policy;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Deletes the table's policy, so it denies anon and signed-in requests
|
|
1379
|
+
* again. `POLICY_NOT_FOUND` when the table has none.
|
|
1380
|
+
*/
|
|
1381
|
+
async removePolicy(projectId, table) {
|
|
1382
|
+
const answer = await this.client.request(
|
|
1383
|
+
this.path(projectId, `/policies/${encodeURIComponent(table)}`),
|
|
1384
|
+
{ method: "DELETE", context: "Remove policy" }
|
|
1385
|
+
);
|
|
1386
|
+
return answer.policy;
|
|
1387
|
+
}
|
|
1118
1388
|
};
|
|
1119
1389
|
|
|
1120
1390
|
// ../../packages/sdk/src/database.ts
|
|
@@ -1142,27 +1412,203 @@ var MAX_FILE_BYTES = 1024 * 1024;
|
|
|
1142
1412
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
1143
1413
|
import { join as join4 } from "path";
|
|
1144
1414
|
|
|
1415
|
+
// ../../packages/data-core/src/system-tables.ts
|
|
1416
|
+
var SYSTEM_TABLE_PREFIXES = [
|
|
1417
|
+
"_naro_",
|
|
1418
|
+
"_cf_",
|
|
1419
|
+
"sqlite_"
|
|
1420
|
+
];
|
|
1421
|
+
var SYSTEM_TABLE_NAMES = ["d1_migrations"];
|
|
1422
|
+
function asciiLower(text) {
|
|
1423
|
+
return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
|
|
1424
|
+
}
|
|
1425
|
+
function likeLiteral(text) {
|
|
1426
|
+
return text.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
|
|
1427
|
+
}
|
|
1428
|
+
var COLUMN_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
1429
|
+
function systemTablePredicate(column = "name") {
|
|
1430
|
+
if (!COLUMN_REFERENCE.test(column))
|
|
1431
|
+
throw new Error(`Not a column reference: ${column}`);
|
|
1432
|
+
return [
|
|
1433
|
+
...SYSTEM_TABLE_PREFIXES.map(
|
|
1434
|
+
(prefix) => `${column} NOT LIKE '${likeLiteral(prefix)}%' ESCAPE '\\'`
|
|
1435
|
+
),
|
|
1436
|
+
...SYSTEM_TABLE_NAMES.map(
|
|
1437
|
+
(name) => `${column} NOT LIKE '${likeLiteral(name)}' ESCAPE '\\'`
|
|
1438
|
+
),
|
|
1439
|
+
`${column} NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow')`
|
|
1440
|
+
].join(" AND ");
|
|
1441
|
+
}
|
|
1442
|
+
var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
|
|
1443
|
+
|
|
1444
|
+
// ../../packages/data-core/src/query.ts
|
|
1445
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
1446
|
+
|
|
1447
|
+
// ../../packages/data-core/src/schema.ts
|
|
1448
|
+
var SCHEMA_SQL = [
|
|
1449
|
+
`SELECT t.name AS table_name, t.type AS table_type, c.cid AS cid, c.name AS column_name, c.type AS column_type, c."notnull" AS not_null, c.dflt_value AS default_value, c.pk AS pk, c.hidden AS hidden FROM pragma_table_list AS t JOIN pragma_table_xinfo(t.name, t.schema) AS c WHERE t.schema = 'main' AND t.type IN ('table', 'virtual') AND ${systemTablePredicate("t.name")} ORDER BY t.name, c.cid`,
|
|
1450
|
+
// Phase 2-2 (적대적 리뷰 반영): each table's CREATE TABLE text, where
|
|
1451
|
+
// alone a column's collation is written. A separate statement, so the
|
|
1452
|
+
// text comes once per table rather than once per column. D1 answers
|
|
1453
|
+
// `sqlite_master` reads (the database routes' `_tables.ts` reads it too).
|
|
1454
|
+
`SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`
|
|
1455
|
+
];
|
|
1456
|
+
|
|
1457
|
+
// ../../packages/data-core/src/condition.ts
|
|
1458
|
+
var MAX_CONDITION_BYTES = 16 * 1024;
|
|
1459
|
+
|
|
1460
|
+
// ../../packages/data-core/src/system-sql.ts
|
|
1461
|
+
var MIGRATIONS_TABLE = "CREATE TABLE IF NOT EXISTS _naro_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT (datetime('now')))";
|
|
1462
|
+
var SYSTEM_MIGRATIONS = [
|
|
1463
|
+
{
|
|
1464
|
+
version: 1,
|
|
1465
|
+
name: "system_tables",
|
|
1466
|
+
statements: [
|
|
1467
|
+
MIGRATIONS_TABLE,
|
|
1468
|
+
"CREATE TABLE IF NOT EXISTS _naro_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))",
|
|
1469
|
+
// Spec §11.2, as written there.
|
|
1470
|
+
"CREATE TABLE IF NOT EXISTS _naro_policies (table_name TEXT PRIMARY KEY, select_rule TEXT NOT NULL DEFAULT 'none', insert_rule TEXT NOT NULL DEFAULT 'none', update_rule TEXT NOT NULL DEFAULT 'none', delete_rule TEXT NOT NULL DEFAULT 'none', owner_column TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')))"
|
|
1471
|
+
]
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
// Spec P2-2 §3.1: an action whose rule column says 'condition' reads its
|
|
1475
|
+
// condition from these. A worker from before them reads 'condition' as
|
|
1476
|
+
// an unknown rule — none.
|
|
1477
|
+
version: 2,
|
|
1478
|
+
name: "policy_conditions",
|
|
1479
|
+
statements: [],
|
|
1480
|
+
addColumns: ["select", "insert", "update", "delete"].map((action) => ({
|
|
1481
|
+
table: "_naro_policies",
|
|
1482
|
+
column: `${action}_condition`,
|
|
1483
|
+
definition: "TEXT"
|
|
1484
|
+
}))
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
// Spec P2-6 §11: the Naro worker's sign-in handoff. A provider sign-in's
|
|
1488
|
+
// flow (state → the app's redirect and the browser's PKCE challenge), the
|
|
1489
|
+
// one-time codes a redirect hands the app, the sends the per-address
|
|
1490
|
+
// email limit counts, and (adversarial review I3) the unconfirmed
|
|
1491
|
+
// accounts someone else also tried to sign up as. Times are epoch
|
|
1492
|
+
// milliseconds.
|
|
1493
|
+
version: 3,
|
|
1494
|
+
name: "auth_handoff",
|
|
1495
|
+
statements: [
|
|
1496
|
+
"CREATE TABLE IF NOT EXISTS _naro_auth_flows (state TEXT PRIMARY KEY, provider TEXT NOT NULL, code_challenge TEXT NOT NULL, redirect_to TEXT NOT NULL, expires_at INTEGER NOT NULL)",
|
|
1497
|
+
"CREATE TABLE IF NOT EXISTS _naro_auth_codes (code_hash TEXT PRIMARY KEY, code_challenge TEXT NOT NULL, session_token TEXT NOT NULL, sealed_token TEXT NOT NULL, expires_at INTEGER NOT NULL)",
|
|
1498
|
+
"CREATE TABLE IF NOT EXISTS _naro_auth_email_log (address_hash TEXT NOT NULL, sent_at INTEGER NOT NULL)",
|
|
1499
|
+
"CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
|
|
1500
|
+
"CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
|
|
1501
|
+
]
|
|
1502
|
+
}
|
|
1503
|
+
];
|
|
1504
|
+
|
|
1505
|
+
// ../../packages/data-core/src/write-quota.ts
|
|
1506
|
+
var WRITE_QUOTA_KEY = "write_quota";
|
|
1507
|
+
var WRITE_QUOTA_SQL = {
|
|
1508
|
+
read: `SELECT value FROM _naro_meta WHERE key = '${WRITE_QUOTA_KEY}'`,
|
|
1509
|
+
upsert: `INSERT INTO _naro_meta (key, value, updated_at) VALUES ('${WRITE_QUOTA_KEY}', ?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1145
1512
|
// ../../packages/sdk/src/generate-types.ts
|
|
1146
1513
|
var HEADER = [
|
|
1147
1514
|
"// Generated by `naro gen types`. Do not edit - re-run the command instead.",
|
|
1148
1515
|
"// `D1Database` comes from @cloudflare/workers-types."
|
|
1149
1516
|
];
|
|
1150
|
-
function
|
|
1517
|
+
function affinity(declared, strict) {
|
|
1151
1518
|
const type = declared.toUpperCase();
|
|
1152
|
-
if (type.trim() === "") return "
|
|
1153
|
-
if (type.
|
|
1519
|
+
if (type.trim() === "") return "NONE";
|
|
1520
|
+
if (strict && type.trim() === "ANY") return "ANY";
|
|
1521
|
+
if (type.includes("INT")) return "INTEGER";
|
|
1154
1522
|
if (type.includes("CHAR") || type.includes("CLOB") || type.includes("TEXT"))
|
|
1155
|
-
return "
|
|
1156
|
-
if (type.includes("BLOB")) return "
|
|
1523
|
+
return "TEXT";
|
|
1524
|
+
if (type.includes("BLOB")) return "BLOB";
|
|
1157
1525
|
if (type.includes("REAL") || type.includes("FLOA") || type.includes("DOUB"))
|
|
1158
|
-
return "
|
|
1159
|
-
|
|
1526
|
+
return "REAL";
|
|
1527
|
+
if (type.includes("BOOL")) return "BOOLEAN";
|
|
1528
|
+
if (type.includes("JSON")) return "JSON";
|
|
1529
|
+
return "NUMERIC";
|
|
1530
|
+
}
|
|
1531
|
+
var READ = {
|
|
1532
|
+
INTEGER: "number",
|
|
1533
|
+
TEXT: "string",
|
|
1534
|
+
BLOB: "number[]",
|
|
1535
|
+
REAL: "number",
|
|
1536
|
+
NONE: "number | string | number[]",
|
|
1537
|
+
ANY: "number | string | number[]",
|
|
1538
|
+
BOOLEAN: "number",
|
|
1539
|
+
JSON: "number | string",
|
|
1540
|
+
NUMERIC: "number | string"
|
|
1541
|
+
};
|
|
1542
|
+
var WRITE = {
|
|
1543
|
+
INTEGER: "number",
|
|
1544
|
+
TEXT: "string | Json",
|
|
1545
|
+
BLOB: "never",
|
|
1546
|
+
REAL: "number",
|
|
1547
|
+
NONE: "number | string | Json",
|
|
1548
|
+
ANY: "number | string | Json",
|
|
1549
|
+
BOOLEAN: "number | boolean",
|
|
1550
|
+
JSON: "number | string | Json",
|
|
1551
|
+
NUMERIC: "number | string"
|
|
1552
|
+
};
|
|
1553
|
+
var JSON_TYPE = "export type Json = readonly unknown[] | { readonly [key: string]: unknown };";
|
|
1554
|
+
function withNull(type, nullable2) {
|
|
1555
|
+
if (!nullable2) return type;
|
|
1556
|
+
return type === "never" ? "null" : `${type} | null`;
|
|
1160
1557
|
}
|
|
1161
1558
|
function interfaceBase(tableName) {
|
|
1162
1559
|
const pascal = tableName.split(/[^A-Za-z0-9$]+/).filter((word) => word.length > 0).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("").replace(/[^A-Za-z0-9_$]/g, "");
|
|
1163
1560
|
return pascal === "" || /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
|
|
1164
1561
|
}
|
|
1562
|
+
function sqlSkeleton(sql) {
|
|
1563
|
+
let out = "";
|
|
1564
|
+
let index = 0;
|
|
1565
|
+
while (index < sql.length) {
|
|
1566
|
+
const char = sql[index] ?? "";
|
|
1567
|
+
const pair = sql.slice(index, index + 2);
|
|
1568
|
+
if (pair === "--") {
|
|
1569
|
+
const end = sql.indexOf("\n", index);
|
|
1570
|
+
index = end < 0 ? sql.length : end;
|
|
1571
|
+
out += " ";
|
|
1572
|
+
} else if (pair === "/*") {
|
|
1573
|
+
const end = sql.indexOf("*/", index + 2);
|
|
1574
|
+
index = end < 0 ? sql.length : end + 2;
|
|
1575
|
+
out += " ";
|
|
1576
|
+
} else if (char === "'" || char === '"' || char === "`" || char === "[") {
|
|
1577
|
+
const close = char === "[" ? "]" : char;
|
|
1578
|
+
let end = index + 1;
|
|
1579
|
+
for (; ; ) {
|
|
1580
|
+
const found = sql.indexOf(close, end);
|
|
1581
|
+
if (found < 0) {
|
|
1582
|
+
end = sql.length;
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
if (close !== "]" && sql[found + 1] === close) {
|
|
1586
|
+
end = found + 2;
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
end = found + 1;
|
|
1590
|
+
break;
|
|
1591
|
+
}
|
|
1592
|
+
out += `${char}${close}`;
|
|
1593
|
+
index = end;
|
|
1594
|
+
} else {
|
|
1595
|
+
out += char;
|
|
1596
|
+
index += 1;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return out;
|
|
1600
|
+
}
|
|
1601
|
+
function tableOptions(sql) {
|
|
1602
|
+
const skeleton = sql === null ? "" : sqlSkeleton(sql);
|
|
1603
|
+
const close = skeleton.lastIndexOf(")");
|
|
1604
|
+
const tail = close < 0 ? "" : skeleton.slice(close + 1);
|
|
1605
|
+
return {
|
|
1606
|
+
withoutRowid: /\bWITHOUT\s+ROWID\b/i.test(tail),
|
|
1607
|
+
strict: /\bSTRICT\b/i.test(tail)
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1165
1610
|
function rowidAlias(table) {
|
|
1611
|
+
if (tableOptions(table.sql).withoutRowid) return null;
|
|
1166
1612
|
const keys = table.columns.filter((column) => column.primaryKey);
|
|
1167
1613
|
const only = keys.length === 1 ? keys[0] : void 0;
|
|
1168
1614
|
return only && only.type.trim().toUpperCase() === "INTEGER" ? only.name : null;
|
|
@@ -1174,34 +1620,120 @@ function keyLiteral(name) {
|
|
|
1174
1620
|
(unit) => `${UNICODE_ESCAPE}${unit.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
1175
1621
|
);
|
|
1176
1622
|
}
|
|
1177
|
-
function
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1623
|
+
function ownerFilledOnInsert(policies, table) {
|
|
1624
|
+
const folded = asciiLower(table);
|
|
1625
|
+
const matches = policies.filter(
|
|
1626
|
+
(policy2) => asciiLower(policy2.table) === folded
|
|
1627
|
+
);
|
|
1628
|
+
const policy = policies.find((candidate) => candidate.table === table) ?? (matches.length === 1 ? matches[0] : void 0);
|
|
1629
|
+
return policy?.insert === "owner" ? policy.ownerColumn : null;
|
|
1630
|
+
}
|
|
1631
|
+
async function policiesForTypes(client, projectId) {
|
|
1632
|
+
try {
|
|
1633
|
+
return { policies: await client.backend.policies(projectId) };
|
|
1634
|
+
} catch (error51) {
|
|
1635
|
+
return {
|
|
1636
|
+
policies: void 0,
|
|
1637
|
+
reason: isNaroError(error51) ? error51.code : "UNKNOWN_ERROR"
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
function isNullable(column, alias) {
|
|
1642
|
+
return !column.notNull && column.name !== alias;
|
|
1643
|
+
}
|
|
1644
|
+
function rowProperty(column, { alias, strict }) {
|
|
1645
|
+
const type = READ[affinity(column.type, strict)];
|
|
1646
|
+
return ` ${keyLiteral(column.name)}: ${withNull(type, isNullable(column, alias))};`;
|
|
1647
|
+
}
|
|
1648
|
+
function writeProperty(column, { alias, strict }, optional2) {
|
|
1649
|
+
const type = WRITE[affinity(column.type, strict)];
|
|
1650
|
+
return ` ${keyLiteral(column.name)}${optional2 ? "?" : ""}: ${withNull(type, isNullable(column, alias))};`;
|
|
1180
1651
|
}
|
|
1181
1652
|
function block(name, lines) {
|
|
1182
1653
|
return lines.length === 0 ? [`export interface ${name} {}`] : [`export interface ${name} {`, ...lines, "}"];
|
|
1183
1654
|
}
|
|
1184
|
-
function
|
|
1655
|
+
function member(indent, name, lines) {
|
|
1656
|
+
return lines.length === 0 ? [`${indent}${name}: {};`] : [`${indent}${name}: {`, ...lines, `${indent}};`];
|
|
1657
|
+
}
|
|
1658
|
+
function tableEntry(described) {
|
|
1659
|
+
const { table, row, alias, owner } = described;
|
|
1660
|
+
const writable = table.columns.filter((column) => column.generated !== true);
|
|
1661
|
+
const optionalOnInsert = (column) => isNullable(column, alias) || column.defaultValue !== null || column.name === alias || asciiLower(column.name) === owner;
|
|
1662
|
+
return [
|
|
1663
|
+
` ${keyLiteral(table.name)}: {`,
|
|
1664
|
+
` Row: ${row};`,
|
|
1665
|
+
...member(
|
|
1666
|
+
" ",
|
|
1667
|
+
"Insert",
|
|
1668
|
+
writable.map(
|
|
1669
|
+
(column) => writeProperty(column, described, optionalOnInsert(column))
|
|
1670
|
+
)
|
|
1671
|
+
),
|
|
1672
|
+
...member(
|
|
1673
|
+
" ",
|
|
1674
|
+
"Update",
|
|
1675
|
+
writable.map((column) => writeProperty(column, described, true))
|
|
1676
|
+
),
|
|
1677
|
+
" };"
|
|
1678
|
+
];
|
|
1679
|
+
}
|
|
1680
|
+
function viewEntry({ table, row }) {
|
|
1681
|
+
return [` ${keyLiteral(table.name)}: {`, ` Row: ${row};`, " };"];
|
|
1682
|
+
}
|
|
1683
|
+
var ALIASES = [
|
|
1684
|
+
'export type Tables<Name extends keyof Database["tables"] | keyof Database["views"]> =',
|
|
1685
|
+
' (Database["tables"] & Database["views"])[Name]["Row"];',
|
|
1686
|
+
'export type TablesInsert<Name extends keyof Database["tables"]> =',
|
|
1687
|
+
' Database["tables"][Name]["Insert"];',
|
|
1688
|
+
'export type TablesUpdate<Name extends keyof Database["tables"]> =',
|
|
1689
|
+
' Database["tables"][Name]["Update"];'
|
|
1690
|
+
];
|
|
1691
|
+
function generateTypes(tables, options2 = {}) {
|
|
1185
1692
|
const used = /* @__PURE__ */ new Set();
|
|
1186
1693
|
const out = [...HEADER];
|
|
1187
|
-
const
|
|
1694
|
+
const described = [];
|
|
1188
1695
|
for (const table of tables) {
|
|
1189
1696
|
const base = `${interfaceBase(table.name)}Row`;
|
|
1190
|
-
let
|
|
1191
|
-
for (let suffix = 2; used.has(
|
|
1192
|
-
|
|
1193
|
-
used.add(
|
|
1194
|
-
const
|
|
1697
|
+
let row = base;
|
|
1698
|
+
for (let suffix = 2; used.has(row); suffix += 1)
|
|
1699
|
+
row = `${base}_${String(suffix)}`;
|
|
1700
|
+
used.add(row);
|
|
1701
|
+
const owner = ownerFilledOnInsert(options2.policies ?? [], table.name);
|
|
1702
|
+
const entry = {
|
|
1703
|
+
table,
|
|
1704
|
+
row,
|
|
1705
|
+
alias: rowidAlias(table),
|
|
1706
|
+
strict: tableOptions(table.sql).strict,
|
|
1707
|
+
owner: owner === null ? null : asciiLower(owner)
|
|
1708
|
+
};
|
|
1709
|
+
described.push(entry);
|
|
1195
1710
|
out.push(
|
|
1196
1711
|
"",
|
|
1197
1712
|
...block(
|
|
1198
|
-
|
|
1199
|
-
table.columns.map((column) =>
|
|
1713
|
+
row,
|
|
1714
|
+
table.columns.map((column) => rowProperty(column, entry))
|
|
1200
1715
|
)
|
|
1201
1716
|
);
|
|
1202
|
-
map2.push(` ${keyLiteral(table.name)}: ${name};`);
|
|
1203
1717
|
}
|
|
1204
|
-
out.push(
|
|
1718
|
+
out.push(
|
|
1719
|
+
"",
|
|
1720
|
+
JSON_TYPE,
|
|
1721
|
+
"",
|
|
1722
|
+
"export type Database = {",
|
|
1723
|
+
...member(
|
|
1724
|
+
" ",
|
|
1725
|
+
"tables",
|
|
1726
|
+
described.filter(({ table }) => table.type !== "view").flatMap(tableEntry)
|
|
1727
|
+
),
|
|
1728
|
+
...member(
|
|
1729
|
+
" ",
|
|
1730
|
+
"views",
|
|
1731
|
+
described.filter(({ table }) => table.type === "view").flatMap(viewEntry)
|
|
1732
|
+
),
|
|
1733
|
+
"};",
|
|
1734
|
+
"",
|
|
1735
|
+
...ALIASES
|
|
1736
|
+
);
|
|
1205
1737
|
out.push("", ...block("Env", [" DB: D1Database;"]));
|
|
1206
1738
|
return `${out.join("\n")}
|
|
1207
1739
|
`;
|
|
@@ -1245,6 +1777,33 @@ function pathnameOf(url2) {
|
|
|
1245
1777
|
// ../../packages/core/src/internal-auth.ts
|
|
1246
1778
|
import { Buffer as Buffer2 } from "buffer";
|
|
1247
1779
|
|
|
1780
|
+
// ../../packages/core/src/migration-names.ts
|
|
1781
|
+
function normalizeMigrationName(name) {
|
|
1782
|
+
return name.trim().toLowerCase().replace(/\.sql$/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1783
|
+
}
|
|
1784
|
+
function leadingMigrationNumber(name) {
|
|
1785
|
+
const match = /^(\d+)/.exec(name);
|
|
1786
|
+
return match ? BigInt(match[1] ?? "0") : null;
|
|
1787
|
+
}
|
|
1788
|
+
function highestMigrationNumber(names) {
|
|
1789
|
+
let max = 0n;
|
|
1790
|
+
for (const name of names) {
|
|
1791
|
+
const number4 = leadingMigrationNumber(name);
|
|
1792
|
+
if (number4 !== null && number4 > max) max = number4;
|
|
1793
|
+
}
|
|
1794
|
+
return max;
|
|
1795
|
+
}
|
|
1796
|
+
function numberedMigrationName(existing, title) {
|
|
1797
|
+
const number4 = (highestMigrationNumber(existing) + 1n).toString().padStart(4, "0");
|
|
1798
|
+
return `${number4}_${title}.sql`;
|
|
1799
|
+
}
|
|
1800
|
+
var MAX_SCHEMA_TITLE_LENGTH = 180;
|
|
1801
|
+
function schemaMigrationTitle(operation) {
|
|
1802
|
+
const op = typeof operation.op === "string" ? operation.op.replace(/[A-Z]/g, (letter) => `_${letter}`) : "";
|
|
1803
|
+
const target = typeof operation.table === "string" ? operation.table : typeof operation.name === "string" ? operation.name : "";
|
|
1804
|
+
return normalizeMigrationName(`schema ${op} ${target}`).slice(0, MAX_SCHEMA_TITLE_LENGTH).replace(/_+$/, "");
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1248
1807
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
1249
1808
|
var external_exports = {};
|
|
1250
1809
|
__export(external_exports, {
|
|
@@ -1881,7 +2440,7 @@ __export(util_exports, {
|
|
|
1881
2440
|
getSizableOrigin: () => getSizableOrigin,
|
|
1882
2441
|
hexToUint8Array: () => hexToUint8Array,
|
|
1883
2442
|
isObject: () => isObject,
|
|
1884
|
-
isPlainObject: () =>
|
|
2443
|
+
isPlainObject: () => isPlainObject2,
|
|
1885
2444
|
issue: () => issue,
|
|
1886
2445
|
joinValues: () => joinValues,
|
|
1887
2446
|
jsonStringifyReplacer: () => jsonStringifyReplacer,
|
|
@@ -2061,7 +2620,7 @@ var allowsEval = /* @__PURE__ */ cached(() => {
|
|
|
2061
2620
|
return false;
|
|
2062
2621
|
}
|
|
2063
2622
|
});
|
|
2064
|
-
function
|
|
2623
|
+
function isPlainObject2(o) {
|
|
2065
2624
|
if (isObject(o) === false)
|
|
2066
2625
|
return false;
|
|
2067
2626
|
const ctor = o.constructor;
|
|
@@ -2078,7 +2637,7 @@ function isPlainObject(o) {
|
|
|
2078
2637
|
return true;
|
|
2079
2638
|
}
|
|
2080
2639
|
function shallowClone(o) {
|
|
2081
|
-
if (
|
|
2640
|
+
if (isPlainObject2(o))
|
|
2082
2641
|
return { ...o };
|
|
2083
2642
|
if (Array.isArray(o))
|
|
2084
2643
|
return [...o];
|
|
@@ -2282,7 +2841,7 @@ function omit(schema, mask) {
|
|
|
2282
2841
|
return clone(schema, def);
|
|
2283
2842
|
}
|
|
2284
2843
|
function extend(schema, shape) {
|
|
2285
|
-
if (!
|
|
2844
|
+
if (!isPlainObject2(shape)) {
|
|
2286
2845
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
2287
2846
|
}
|
|
2288
2847
|
const checks = schema._zod.def.checks;
|
|
@@ -2305,7 +2864,7 @@ function extend(schema, shape) {
|
|
|
2305
2864
|
return clone(schema, def);
|
|
2306
2865
|
}
|
|
2307
2866
|
function safeExtend(schema, shape) {
|
|
2308
|
-
if (!
|
|
2867
|
+
if (!isPlainObject2(shape)) {
|
|
2309
2868
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
2310
2869
|
}
|
|
2311
2870
|
const def = mergeDefs(schema._zod.def, {
|
|
@@ -3430,9 +3989,9 @@ var $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst,
|
|
|
3430
3989
|
});
|
|
3431
3990
|
};
|
|
3432
3991
|
});
|
|
3433
|
-
function handleCheckPropertyResult(result, payload,
|
|
3992
|
+
function handleCheckPropertyResult(result, payload, property) {
|
|
3434
3993
|
if (result.issues.length) {
|
|
3435
|
-
payload.issues.push(...prefixIssues(
|
|
3994
|
+
payload.issues.push(...prefixIssues(property, result.issues));
|
|
3436
3995
|
}
|
|
3437
3996
|
}
|
|
3438
3997
|
var $ZodCheckProperty = /* @__PURE__ */ $constructor("$ZodCheckProperty", (inst, def) => {
|
|
@@ -4652,7 +5211,7 @@ function mergeValues(a, b) {
|
|
|
4652
5211
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
4653
5212
|
return { valid: true, data: a };
|
|
4654
5213
|
}
|
|
4655
|
-
if (
|
|
5214
|
+
if (isPlainObject2(a) && isPlainObject2(b)) {
|
|
4656
5215
|
const bKeys = Object.keys(b);
|
|
4657
5216
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
4658
5217
|
const newObj = { ...a, ...b };
|
|
@@ -4838,7 +5397,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
4838
5397
|
$ZodType.init(inst, def);
|
|
4839
5398
|
inst._zod.parse = (payload, ctx) => {
|
|
4840
5399
|
const input = payload.value;
|
|
4841
|
-
if (!
|
|
5400
|
+
if (!isPlainObject2(input)) {
|
|
4842
5401
|
payload.issues.push({
|
|
4843
5402
|
expected: "record",
|
|
4844
5403
|
code: "invalid_type",
|
|
@@ -12259,10 +12818,10 @@ function _endsWith(suffix, params) {
|
|
|
12259
12818
|
});
|
|
12260
12819
|
}
|
|
12261
12820
|
// @__NO_SIDE_EFFECTS__
|
|
12262
|
-
function _property(
|
|
12821
|
+
function _property(property, schema, params) {
|
|
12263
12822
|
return new $ZodCheckProperty({
|
|
12264
12823
|
check: "property",
|
|
12265
|
-
property
|
|
12824
|
+
property,
|
|
12266
12825
|
schema,
|
|
12267
12826
|
...normalizeParams(params)
|
|
12268
12827
|
});
|
|
@@ -12359,28 +12918,28 @@ function _tuple(Class2, items, _paramsOrRest, _params) {
|
|
|
12359
12918
|
});
|
|
12360
12919
|
}
|
|
12361
12920
|
// @__NO_SIDE_EFFECTS__
|
|
12362
|
-
function _record(Class2, keyType,
|
|
12921
|
+
function _record(Class2, keyType, valueType, params) {
|
|
12363
12922
|
return new Class2({
|
|
12364
12923
|
type: "record",
|
|
12365
12924
|
keyType,
|
|
12366
|
-
valueType
|
|
12925
|
+
valueType,
|
|
12367
12926
|
...normalizeParams(params)
|
|
12368
12927
|
});
|
|
12369
12928
|
}
|
|
12370
12929
|
// @__NO_SIDE_EFFECTS__
|
|
12371
|
-
function _map(Class2, keyType,
|
|
12930
|
+
function _map(Class2, keyType, valueType, params) {
|
|
12372
12931
|
return new Class2({
|
|
12373
12932
|
type: "map",
|
|
12374
12933
|
keyType,
|
|
12375
|
-
valueType
|
|
12934
|
+
valueType,
|
|
12376
12935
|
...normalizeParams(params)
|
|
12377
12936
|
});
|
|
12378
12937
|
}
|
|
12379
12938
|
// @__NO_SIDE_EFFECTS__
|
|
12380
|
-
function _set(Class2,
|
|
12939
|
+
function _set(Class2, valueType, params) {
|
|
12381
12940
|
return new Class2({
|
|
12382
12941
|
type: "set",
|
|
12383
|
-
valueType
|
|
12942
|
+
valueType,
|
|
12384
12943
|
...normalizeParams(params)
|
|
12385
12944
|
});
|
|
12386
12945
|
}
|
|
@@ -14746,37 +15305,37 @@ var ZodRecord = /* @__PURE__ */ $constructor("ZodRecord", (inst, def) => {
|
|
|
14746
15305
|
inst.keyType = def.keyType;
|
|
14747
15306
|
inst.valueType = def.valueType;
|
|
14748
15307
|
});
|
|
14749
|
-
function record(keyType,
|
|
14750
|
-
if (!
|
|
15308
|
+
function record(keyType, valueType, params) {
|
|
15309
|
+
if (!valueType || !valueType._zod) {
|
|
14751
15310
|
return new ZodRecord({
|
|
14752
15311
|
type: "record",
|
|
14753
15312
|
keyType: string2(),
|
|
14754
15313
|
valueType: keyType,
|
|
14755
|
-
...util_exports.normalizeParams(
|
|
15314
|
+
...util_exports.normalizeParams(valueType)
|
|
14756
15315
|
});
|
|
14757
15316
|
}
|
|
14758
15317
|
return new ZodRecord({
|
|
14759
15318
|
type: "record",
|
|
14760
15319
|
keyType,
|
|
14761
|
-
valueType
|
|
15320
|
+
valueType,
|
|
14762
15321
|
...util_exports.normalizeParams(params)
|
|
14763
15322
|
});
|
|
14764
15323
|
}
|
|
14765
|
-
function partialRecord(keyType,
|
|
15324
|
+
function partialRecord(keyType, valueType, params) {
|
|
14766
15325
|
const k = clone(keyType);
|
|
14767
15326
|
k._zod.values = void 0;
|
|
14768
15327
|
return new ZodRecord({
|
|
14769
15328
|
type: "record",
|
|
14770
15329
|
keyType: k,
|
|
14771
|
-
valueType
|
|
15330
|
+
valueType,
|
|
14772
15331
|
...util_exports.normalizeParams(params)
|
|
14773
15332
|
});
|
|
14774
15333
|
}
|
|
14775
|
-
function looseRecord(keyType,
|
|
15334
|
+
function looseRecord(keyType, valueType, params) {
|
|
14776
15335
|
return new ZodRecord({
|
|
14777
15336
|
type: "record",
|
|
14778
15337
|
keyType,
|
|
14779
|
-
valueType
|
|
15338
|
+
valueType,
|
|
14780
15339
|
mode: "loose",
|
|
14781
15340
|
...util_exports.normalizeParams(params)
|
|
14782
15341
|
});
|
|
@@ -14792,11 +15351,11 @@ var ZodMap = /* @__PURE__ */ $constructor("ZodMap", (inst, def) => {
|
|
|
14792
15351
|
inst.max = (...args) => inst.check(_maxSize(...args));
|
|
14793
15352
|
inst.size = (...args) => inst.check(_size(...args));
|
|
14794
15353
|
});
|
|
14795
|
-
function map(keyType,
|
|
15354
|
+
function map(keyType, valueType, params) {
|
|
14796
15355
|
return new ZodMap({
|
|
14797
15356
|
type: "map",
|
|
14798
15357
|
keyType,
|
|
14799
|
-
valueType
|
|
15358
|
+
valueType,
|
|
14800
15359
|
...util_exports.normalizeParams(params)
|
|
14801
15360
|
});
|
|
14802
15361
|
}
|
|
@@ -14809,10 +15368,10 @@ var ZodSet = /* @__PURE__ */ $constructor("ZodSet", (inst, def) => {
|
|
|
14809
15368
|
inst.max = (...args) => inst.check(_maxSize(...args));
|
|
14810
15369
|
inst.size = (...args) => inst.check(_size(...args));
|
|
14811
15370
|
});
|
|
14812
|
-
function set(
|
|
15371
|
+
function set(valueType, params) {
|
|
14813
15372
|
return new ZodSet({
|
|
14814
15373
|
type: "set",
|
|
14815
|
-
valueType
|
|
15374
|
+
valueType,
|
|
14816
15375
|
...util_exports.normalizeParams(params)
|
|
14817
15376
|
});
|
|
14818
15377
|
}
|
|
@@ -15784,12 +16343,15 @@ var RESERVED_SUBDOMAINS = [
|
|
|
15784
16343
|
"support",
|
|
15785
16344
|
"www"
|
|
15786
16345
|
];
|
|
16346
|
+
var BACKEND_WORKER_NAME_PREFIX = "naro-backend-";
|
|
15787
16347
|
var SUBDOMAIN_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
|
|
15788
16348
|
var SUBDOMAIN_MAX_LENGTH = 54;
|
|
15789
16349
|
function subdomainProblem(value) {
|
|
15790
16350
|
if (value.length < 1 || value.length > SUBDOMAIN_MAX_LENGTH) return "invalid";
|
|
15791
16351
|
if (!SUBDOMAIN_PATTERN.test(value)) return "invalid";
|
|
15792
16352
|
if (value.endsWith("-preview")) return "reserved";
|
|
16353
|
+
if (value === BACKEND_WORKER_NAME_PREFIX.slice(0, -1) || value.startsWith(BACKEND_WORKER_NAME_PREFIX))
|
|
16354
|
+
return "reserved";
|
|
15793
16355
|
if (RESERVED_SUBDOMAINS.includes(value))
|
|
15794
16356
|
return "reserved";
|
|
15795
16357
|
return null;
|
|
@@ -15913,29 +16475,17 @@ function resolveMigrationsDir(input) {
|
|
|
15913
16475
|
if (input.dir) return resolve(input.baseDir, input.dir);
|
|
15914
16476
|
return resolve(input.baseDir, input.rootDirectory ?? "", "migrations");
|
|
15915
16477
|
}
|
|
15916
|
-
function normalizeMigrationName(name) {
|
|
15917
|
-
return name.trim().toLowerCase().replace(/\.sql$/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
15918
|
-
}
|
|
15919
|
-
function leadingNumber(name) {
|
|
15920
|
-
const match = /^(\d+)/.exec(name);
|
|
15921
|
-
return match ? Number(match[1]) : null;
|
|
15922
|
-
}
|
|
15923
16478
|
function sortMigrationNames(names) {
|
|
15924
16479
|
return names.toSorted((a, b) => {
|
|
15925
|
-
const left =
|
|
15926
|
-
const right =
|
|
15927
|
-
if (left !== null && right !== null && left !== right)
|
|
16480
|
+
const left = leadingMigrationNumber(a);
|
|
16481
|
+
const right = leadingMigrationNumber(b);
|
|
16482
|
+
if (left !== null && right !== null && left !== right)
|
|
16483
|
+
return left < right ? -1 : 1;
|
|
15928
16484
|
if (left === null && right !== null) return 1;
|
|
15929
16485
|
if (left !== null && right === null) return -1;
|
|
15930
16486
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
15931
16487
|
});
|
|
15932
16488
|
}
|
|
15933
|
-
function highestMigrationNumber(names) {
|
|
15934
|
-
return names.reduce(
|
|
15935
|
-
(max, name) => Math.max(max, leadingNumber(name) ?? 0),
|
|
15936
|
-
0
|
|
15937
|
-
);
|
|
15938
|
-
}
|
|
15939
16489
|
function listMigrationFiles(dir) {
|
|
15940
16490
|
if (!existsSync5(dir)) return [];
|
|
15941
16491
|
return sortMigrationNames(
|
|
@@ -15949,8 +16499,57 @@ function nextMigrationFileName(existing, name) {
|
|
|
15949
16499
|
"VALIDATION_FAILED",
|
|
15950
16500
|
"Migration name must contain letters or digits."
|
|
15951
16501
|
);
|
|
15952
|
-
|
|
15953
|
-
|
|
16502
|
+
return numberedMigrationName(existing, normalized);
|
|
16503
|
+
}
|
|
16504
|
+
function compareMigrations(local, applied) {
|
|
16505
|
+
const ledger = new Map(applied.map((row) => [row.name, row]));
|
|
16506
|
+
const highestApplied = highestMigrationNumber(applied.map((row) => row.name));
|
|
16507
|
+
const rows = [];
|
|
16508
|
+
for (const name of local) {
|
|
16509
|
+
const exact = ledger.get(name);
|
|
16510
|
+
if (exact) {
|
|
16511
|
+
ledger.delete(name);
|
|
16512
|
+
rows.push({ name, status: "applied", appliedAt: exact.appliedAt });
|
|
16513
|
+
continue;
|
|
16514
|
+
}
|
|
16515
|
+
const bare = ledger.get(name.replace(/\.sql$/, ""));
|
|
16516
|
+
if (bare) {
|
|
16517
|
+
ledger.delete(bare.name);
|
|
16518
|
+
rows.push({
|
|
16519
|
+
name,
|
|
16520
|
+
status: "applied",
|
|
16521
|
+
appliedAt: bare.appliedAt,
|
|
16522
|
+
warning: "ledger-name-missing-extension"
|
|
16523
|
+
});
|
|
16524
|
+
continue;
|
|
16525
|
+
}
|
|
16526
|
+
const number4 = leadingMigrationNumber(name);
|
|
16527
|
+
rows.push({
|
|
16528
|
+
name,
|
|
16529
|
+
status: "pending",
|
|
16530
|
+
...number4 !== null && number4 < highestApplied ? { warning: "out-of-order" } : {}
|
|
16531
|
+
});
|
|
16532
|
+
}
|
|
16533
|
+
for (const row of ledger.values())
|
|
16534
|
+
rows.push({
|
|
16535
|
+
name: row.name,
|
|
16536
|
+
status: "remote-only",
|
|
16537
|
+
appliedAt: row.appliedAt
|
|
16538
|
+
});
|
|
16539
|
+
const order = sortMigrationNames(rows.map((row) => row.name));
|
|
16540
|
+
return rows.toSorted((a, b) => order.indexOf(a.name) - order.indexOf(b.name));
|
|
16541
|
+
}
|
|
16542
|
+
function pendingMigrationNames(local, applied) {
|
|
16543
|
+
return compareMigrations(local, applied).filter((row) => row.status === "pending").map((row) => row.name);
|
|
16544
|
+
}
|
|
16545
|
+
function migrationsPendingError(pending) {
|
|
16546
|
+
return new NaroError(
|
|
16547
|
+
"DATABASE_MIGRATIONS_PENDING",
|
|
16548
|
+
`Run \`naro db push\` first; ${String(pending.length)} local migration(s) are not applied yet: ${pending.join(", ")}.`
|
|
16549
|
+
);
|
|
16550
|
+
}
|
|
16551
|
+
function migrationNameMismatch(sent, recorded) {
|
|
16552
|
+
return `The control plane recorded the migration as ${recorded}, but naro sent ${sent} and wrote the file under that name. Until the two match, naro migration list shows them apart and naro db push would apply the file again.`;
|
|
15954
16553
|
}
|
|
15955
16554
|
|
|
15956
16555
|
// src/naro.ts
|
|
@@ -16165,6 +16764,179 @@ function staysInsideProject(projectDir, target) {
|
|
|
16165
16764
|
return false;
|
|
16166
16765
|
}
|
|
16167
16766
|
}
|
|
16767
|
+
function assertRootDirectoryArg(args) {
|
|
16768
|
+
if (args.rootDirectory !== void 0 && typeof args.rootDirectory !== "string")
|
|
16769
|
+
throw new Error("rootDirectory must be a string.");
|
|
16770
|
+
}
|
|
16771
|
+
function migrationTarget(args, deps, projectId) {
|
|
16772
|
+
const { dir: projectDir, mismatch } = writableProjectDirectory(
|
|
16773
|
+
deps,
|
|
16774
|
+
str(args.directory),
|
|
16775
|
+
projectId
|
|
16776
|
+
);
|
|
16777
|
+
const rootDirectory = str(args.rootDirectory);
|
|
16778
|
+
if (rootDirectory !== void 0) {
|
|
16779
|
+
const escapes = projectDir !== void 0 && relative(projectDir, resolve2(projectDir, rootDirectory)).startsWith("..");
|
|
16780
|
+
if (isAbsolute(rootDirectory) || escapes) throw new Error(OUTSIDE_PROJECT);
|
|
16781
|
+
}
|
|
16782
|
+
const dir = projectDir ? resolveMigrationsDir({ baseDir: projectDir, rootDirectory }) : void 0;
|
|
16783
|
+
if (dir && projectDir && !staysInsideProject(projectDir, dir))
|
|
16784
|
+
throw new Error(
|
|
16785
|
+
rootDirectory === void 0 ? MIGRATIONS_OUTSIDE_PROJECT : OUTSIDE_PROJECT
|
|
16786
|
+
);
|
|
16787
|
+
return {
|
|
16788
|
+
...projectDir === void 0 ? {} : { projectDir },
|
|
16789
|
+
...dir === void 0 ? {} : { dir },
|
|
16790
|
+
...mismatch === void 0 ? {} : { mismatch }
|
|
16791
|
+
};
|
|
16792
|
+
}
|
|
16793
|
+
async function nextMigrationName(deps, projectId, target, title, options2 = {}) {
|
|
16794
|
+
const local = target.dir ? listMigrationFiles(target.dir) : [];
|
|
16795
|
+
let ledger;
|
|
16796
|
+
try {
|
|
16797
|
+
ledger = await (await deps.client()).db.migrations(projectId);
|
|
16798
|
+
} catch (error51) {
|
|
16799
|
+
if (isNaroError(error51) && error51.code === "DATABASE_NOT_READY") ledger = [];
|
|
16800
|
+
else throw error51;
|
|
16801
|
+
}
|
|
16802
|
+
if (options2.refusePending) {
|
|
16803
|
+
const pending = pendingMigrationNames(local, ledger);
|
|
16804
|
+
if (pending.length > 0) throw migrationsPendingError(pending);
|
|
16805
|
+
}
|
|
16806
|
+
return nextMigrationFileName(
|
|
16807
|
+
[...local, ...ledger.map((migration) => migration.name)],
|
|
16808
|
+
title
|
|
16809
|
+
);
|
|
16810
|
+
}
|
|
16811
|
+
function writeMigrationFile(target, fileName, sql) {
|
|
16812
|
+
const { dir, projectDir } = target;
|
|
16813
|
+
if (dir && projectDir) {
|
|
16814
|
+
const relDir = relative(projectDir, dir);
|
|
16815
|
+
const path = join6(dir, fileName);
|
|
16816
|
+
try {
|
|
16817
|
+
const escaped = new Error(
|
|
16818
|
+
"the migrations directory resolves outside the project directory"
|
|
16819
|
+
);
|
|
16820
|
+
if (!staysInsideProject(projectDir, dir)) throw escaped;
|
|
16821
|
+
mkdirSync3(dir, { recursive: true });
|
|
16822
|
+
if (!staysInsideProject(projectDir, dir)) throw escaped;
|
|
16823
|
+
writeFileSync3(path, sql, { flag: "wx" });
|
|
16824
|
+
return { file: join6(relDir, fileName) };
|
|
16825
|
+
} catch (error51) {
|
|
16826
|
+
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
16827
|
+
return {
|
|
16828
|
+
file: null,
|
|
16829
|
+
fileWarning: `Applied, but the migration file could not be written to ${join6(relDir, fileName)}: ${message}. Create it yourself so naro db push does not see a gap.`
|
|
16830
|
+
};
|
|
16831
|
+
}
|
|
16832
|
+
}
|
|
16833
|
+
if (target.mismatch)
|
|
16834
|
+
return {
|
|
16835
|
+
file: null,
|
|
16836
|
+
fileWarning: `Applied, but no file was written: ${target.mismatch}. Save the SQL as migrations/${fileName} yourself so naro db push does not see a gap.`
|
|
16837
|
+
};
|
|
16838
|
+
return {
|
|
16839
|
+
file: null,
|
|
16840
|
+
fileWarning: `Applied. No project directory is known, so no file was written \u2014 save the SQL as migrations/${fileName} to keep local files and the ledger in step.`
|
|
16841
|
+
};
|
|
16842
|
+
}
|
|
16843
|
+
async function runSchemaTool(args, deps, operation) {
|
|
16844
|
+
if (args.dryRun !== void 0 && typeof args.dryRun !== "boolean")
|
|
16845
|
+
throw new Error("dryRun must be a boolean.");
|
|
16846
|
+
const dryRun = args.dryRun === true;
|
|
16847
|
+
const projectId = await deps.resolveProjectId({
|
|
16848
|
+
projectId: str(args.projectId),
|
|
16849
|
+
directory: str(args.directory)
|
|
16850
|
+
});
|
|
16851
|
+
const target = migrationTarget(args, deps, projectId);
|
|
16852
|
+
const name = await nextMigrationName(
|
|
16853
|
+
deps,
|
|
16854
|
+
projectId,
|
|
16855
|
+
target,
|
|
16856
|
+
schemaMigrationTitle(operation),
|
|
16857
|
+
{ refusePending: !dryRun }
|
|
16858
|
+
);
|
|
16859
|
+
const result = await (await deps.client()).db.schema(projectId, [operation], {
|
|
16860
|
+
...dryRun ? { dryRun: true } : {},
|
|
16861
|
+
name
|
|
16862
|
+
});
|
|
16863
|
+
if (result.dryRun) return { projectId, ...result, file: null };
|
|
16864
|
+
const written = writeMigrationFile(target, name, result.sql);
|
|
16865
|
+
return {
|
|
16866
|
+
projectId,
|
|
16867
|
+
...result,
|
|
16868
|
+
...result.name === name ? {} : {
|
|
16869
|
+
warnings: [
|
|
16870
|
+
...result.warnings,
|
|
16871
|
+
migrationNameMismatch(name, result.name)
|
|
16872
|
+
]
|
|
16873
|
+
},
|
|
16874
|
+
...written
|
|
16875
|
+
};
|
|
16876
|
+
}
|
|
16877
|
+
function assertSchemaArgs(args) {
|
|
16878
|
+
assertProjectArgs(args);
|
|
16879
|
+
assertRootDirectoryArg(args);
|
|
16880
|
+
}
|
|
16881
|
+
function required2(value, message) {
|
|
16882
|
+
const text = str(value);
|
|
16883
|
+
if (text === void 0) throw new Error(message);
|
|
16884
|
+
return text;
|
|
16885
|
+
}
|
|
16886
|
+
var SCHEMA_ARGS = {
|
|
16887
|
+
dryRun: {
|
|
16888
|
+
type: "boolean",
|
|
16889
|
+
description: "Return the SQL and the migration name without applying, recording or writing anything."
|
|
16890
|
+
},
|
|
16891
|
+
rootDirectory: {
|
|
16892
|
+
type: "string",
|
|
16893
|
+
description: `Sub-directory holding the app's migrations/ folder, relative to the project directory, e.g. "apps/web". Must stay inside the project directory. Defaults to the project directory itself.`
|
|
16894
|
+
}
|
|
16895
|
+
};
|
|
16896
|
+
var COLUMN_SCHEMA = {
|
|
16897
|
+
type: "object",
|
|
16898
|
+
properties: {
|
|
16899
|
+
name: { type: "string" },
|
|
16900
|
+
type: {
|
|
16901
|
+
type: "string",
|
|
16902
|
+
enum: [...SCHEMA_COLUMN_TYPES],
|
|
16903
|
+
description: "Declared type. Store JSON in TEXT: a JSON column has NUMERIC affinity and turns '123' into a number."
|
|
16904
|
+
},
|
|
16905
|
+
notNull: { type: "boolean" },
|
|
16906
|
+
default: {
|
|
16907
|
+
description: 'A literal: a string, a finite number, true, false, null, or {"keyword": "CURRENT_TIMESTAMP" | "CURRENT_DATE" | "CURRENT_TIME"}. Never an expression.'
|
|
16908
|
+
},
|
|
16909
|
+
primaryKey: {
|
|
16910
|
+
type: "boolean",
|
|
16911
|
+
description: "Several make a composite key. Every primary key but a single INTEGER one is NOT NULL."
|
|
16912
|
+
},
|
|
16913
|
+
autoIncrement: {
|
|
16914
|
+
type: "boolean",
|
|
16915
|
+
description: "Only on the table's one INTEGER primary key."
|
|
16916
|
+
},
|
|
16917
|
+
unique: { type: "boolean" },
|
|
16918
|
+
references: {
|
|
16919
|
+
type: "object",
|
|
16920
|
+
description: "A foreign key. The parent column must be its table's only primary key or UNIQUE.",
|
|
16921
|
+
properties: {
|
|
16922
|
+
table: { type: "string" },
|
|
16923
|
+
column: { type: "string" },
|
|
16924
|
+
onDelete: {
|
|
16925
|
+
type: "string",
|
|
16926
|
+
enum: ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"]
|
|
16927
|
+
}
|
|
16928
|
+
},
|
|
16929
|
+
required: ["table", "column"]
|
|
16930
|
+
}
|
|
16931
|
+
},
|
|
16932
|
+
required: ["name", "type"]
|
|
16933
|
+
};
|
|
16934
|
+
var ALTER_ACTIONS = [
|
|
16935
|
+
"add_column",
|
|
16936
|
+
"rename_column",
|
|
16937
|
+
"drop_column",
|
|
16938
|
+
"rename_table"
|
|
16939
|
+
];
|
|
16168
16940
|
var DATABASE_TOOLS = [
|
|
16169
16941
|
{
|
|
16170
16942
|
name: "enable_database",
|
|
@@ -16196,7 +16968,7 @@ var DATABASE_TOOLS = [
|
|
|
16196
16968
|
destructiveHint: false,
|
|
16197
16969
|
idempotentHint: true
|
|
16198
16970
|
},
|
|
16199
|
-
description: `Returns the project database's provider, status, size, table count and limits, plus remainingBytes before the 10 GB ceiling (which cannot be raised). ${UNTRUSTED}`,
|
|
16971
|
+
description: `Returns the project database's provider, status, size, table count and limits, plus remainingBytes before the 10 GB ceiling (which cannot be raised). usage is today's (UTC) rowsRead, rowsWritten, storageBytes (today's peak size, which does not drop when data is deleted) and the daily writeLimit, or null when nothing was metered yet; while usage.writesBlocked is true, execute_sql writes and apply_migration fail with DATABASE_WRITE_QUOTA_EXCEEDED until 00:00 UTC \u2014 reads still work. usage.storageWarning is true from 8 GB of usage.storageLimitBytes (10 GB), judged on the current size (cloudflare.fileSize, the one remainingBytes uses) or on today's peak when that is unknown; at the limit every write fails with DATABASE_FULL until data is deleted, so suggest deleting rows or moving large blobs to R2 before then. ${UNTRUSTED}`,
|
|
16200
16972
|
inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
|
|
16201
16973
|
handler: async (args, deps) => {
|
|
16202
16974
|
assertProjectArgs(args);
|
|
@@ -16344,63 +17116,15 @@ var DATABASE_TOOLS = [
|
|
|
16344
17116
|
if (!name) throw new Error("name is required.");
|
|
16345
17117
|
const query = str(args.query);
|
|
16346
17118
|
if (!query) throw new Error("query is required.");
|
|
16347
|
-
|
|
16348
|
-
throw new Error("rootDirectory must be a string.");
|
|
17119
|
+
assertRootDirectoryArg(args);
|
|
16349
17120
|
const projectId = await deps.resolveProjectId({
|
|
16350
17121
|
projectId: str(args.projectId),
|
|
16351
17122
|
directory: str(args.directory)
|
|
16352
17123
|
});
|
|
16353
|
-
const
|
|
16354
|
-
const
|
|
16355
|
-
if (rootDirectory !== void 0) {
|
|
16356
|
-
const escapes = projectDir !== void 0 && relative(projectDir, resolve2(projectDir, rootDirectory)).startsWith(
|
|
16357
|
-
".."
|
|
16358
|
-
);
|
|
16359
|
-
if (isAbsolute(rootDirectory) || escapes)
|
|
16360
|
-
throw new Error(OUTSIDE_PROJECT);
|
|
16361
|
-
}
|
|
16362
|
-
const dir = projectDir ? resolveMigrationsDir({ baseDir: projectDir, rootDirectory }) : void 0;
|
|
16363
|
-
if (dir && projectDir && !staysInsideProject(projectDir, dir))
|
|
16364
|
-
throw new Error(
|
|
16365
|
-
rootDirectory === void 0 ? MIGRATIONS_OUTSIDE_PROJECT : OUTSIDE_PROJECT
|
|
16366
|
-
);
|
|
16367
|
-
const local = dir ? listMigrationFiles(dir) : [];
|
|
16368
|
-
let ledger;
|
|
16369
|
-
try {
|
|
16370
|
-
ledger = await (await deps.client()).db.migrations(projectId);
|
|
16371
|
-
} catch (error51) {
|
|
16372
|
-
if (isNaroError(error51) && error51.code === "DATABASE_NOT_READY")
|
|
16373
|
-
ledger = [];
|
|
16374
|
-
else throw error51;
|
|
16375
|
-
}
|
|
16376
|
-
const fileName = nextMigrationFileName(
|
|
16377
|
-
[...local, ...ledger.map((migration) => migration.name)],
|
|
16378
|
-
name
|
|
16379
|
-
);
|
|
17124
|
+
const target = migrationTarget(args, deps, projectId);
|
|
17125
|
+
const fileName = await nextMigrationName(deps, projectId, target, name);
|
|
16380
17126
|
const applied = await (await deps.client()).db.applyMigration(projectId, { name: fileName, sql: query });
|
|
16381
|
-
|
|
16382
|
-
let fileWarning;
|
|
16383
|
-
if (dir && projectDir) {
|
|
16384
|
-
const relDir = relative(projectDir, dir);
|
|
16385
|
-
const path = join6(dir, fileName);
|
|
16386
|
-
try {
|
|
16387
|
-
const escaped = new Error(
|
|
16388
|
-
"the migrations directory resolves outside the project directory"
|
|
16389
|
-
);
|
|
16390
|
-
if (!staysInsideProject(projectDir, dir)) throw escaped;
|
|
16391
|
-
mkdirSync3(dir, { recursive: true });
|
|
16392
|
-
if (!staysInsideProject(projectDir, dir)) throw escaped;
|
|
16393
|
-
writeFileSync3(path, query, { flag: "wx" });
|
|
16394
|
-
file2 = join6(relDir, fileName);
|
|
16395
|
-
} catch (error51) {
|
|
16396
|
-
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
16397
|
-
fileWarning = `Applied, but the migration file could not be written to ${join6(relDir, fileName)}: ${message}. Create it yourself so naro db push does not see a gap.`;
|
|
16398
|
-
}
|
|
16399
|
-
} else if (directoryMismatch) {
|
|
16400
|
-
fileWarning = `Applied, but no file was written: ${directoryMismatch}. Save the SQL as migrations/${fileName} yourself so naro db push does not see a gap.`;
|
|
16401
|
-
} else {
|
|
16402
|
-
fileWarning = `Applied. No project directory is known, so no file was written \u2014 save the SQL as migrations/${fileName} to keep local files and the ledger in step.`;
|
|
16403
|
-
}
|
|
17127
|
+
const { file: file2, fileWarning } = writeMigrationFile(target, fileName, query);
|
|
16404
17128
|
return {
|
|
16405
17129
|
projectId,
|
|
16406
17130
|
migration: applied.migration,
|
|
@@ -16410,6 +17134,239 @@ var DATABASE_TOOLS = [
|
|
|
16410
17134
|
};
|
|
16411
17135
|
}
|
|
16412
17136
|
},
|
|
17137
|
+
{
|
|
17138
|
+
name: "create_table",
|
|
17139
|
+
group: "database",
|
|
17140
|
+
untrusted: true,
|
|
17141
|
+
annotations: {
|
|
17142
|
+
readOnlyHint: false,
|
|
17143
|
+
destructiveHint: false,
|
|
17144
|
+
idempotentHint: false
|
|
17145
|
+
},
|
|
17146
|
+
description: `Creates a table in the project's D1 database without writing SQL: naro renders the CREATE TABLE from columns, applies it as one migration recorded in d1_migrations and, when the project directory is known, writes it to ./migrations as NNNN_schema_create_table_<table>.sql so naro db push and naro db reset see it. Column types: INTEGER, REAL, TEXT, BLOB, NUMERIC, BOOLEAN, DATETIME, JSON \u2014 store JSON in TEXT, since a JSON column turns '123' into a number. default takes a literal (a string, a number, true, false, null) or {"keyword": "CURRENT_TIMESTAMP"}, never an expression. Every primary key but a single INTEGER one is NOT NULL. owner: {column} adds that column as TEXT NOT NULL and, when the project's backend is ready, an owner policy for select, insert, update and delete. Without a policy the Data API refuses every anon and signed-in request to the table; warnings says so. dryRun: true returns the SQL and the migration name without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
17147
|
+
inputSchema: {
|
|
17148
|
+
type: "object",
|
|
17149
|
+
properties: {
|
|
17150
|
+
...PROJECT_ARGS,
|
|
17151
|
+
table: { type: "string", description: "Name of the new table." },
|
|
17152
|
+
columns: {
|
|
17153
|
+
type: "array",
|
|
17154
|
+
description: "1 to 100 columns (D1's limit), in order.",
|
|
17155
|
+
items: COLUMN_SCHEMA
|
|
17156
|
+
},
|
|
17157
|
+
owner: {
|
|
17158
|
+
type: "object",
|
|
17159
|
+
description: "Adds this column as TEXT NOT NULL (leave it out of columns) and, when the backend is ready, an owner policy on all four actions.",
|
|
17160
|
+
properties: { column: { type: "string" } },
|
|
17161
|
+
required: ["column"]
|
|
17162
|
+
},
|
|
17163
|
+
ifNotExists: {
|
|
17164
|
+
type: "boolean",
|
|
17165
|
+
description: "Leave an existing table of that name as it is."
|
|
17166
|
+
},
|
|
17167
|
+
...SCHEMA_ARGS
|
|
17168
|
+
},
|
|
17169
|
+
required: ["table", "columns"]
|
|
17170
|
+
},
|
|
17171
|
+
handler: async (args, deps) => {
|
|
17172
|
+
assertSchemaArgs(args);
|
|
17173
|
+
const table = required2(args.table, "table is required.");
|
|
17174
|
+
if (!Array.isArray(args.columns)) throw new Error("columns is required.");
|
|
17175
|
+
return runSchemaTool(args, deps, {
|
|
17176
|
+
op: "createTable",
|
|
17177
|
+
table,
|
|
17178
|
+
columns: args.columns,
|
|
17179
|
+
...args.owner !== void 0 ? { owner: args.owner } : {},
|
|
17180
|
+
...args.ifNotExists !== void 0 ? { ifNotExists: args.ifNotExists } : {}
|
|
17181
|
+
});
|
|
17182
|
+
}
|
|
17183
|
+
},
|
|
17184
|
+
{
|
|
17185
|
+
name: "alter_table",
|
|
17186
|
+
group: "database",
|
|
17187
|
+
untrusted: true,
|
|
17188
|
+
annotations: {
|
|
17189
|
+
readOnlyHint: false,
|
|
17190
|
+
destructiveHint: true,
|
|
17191
|
+
idempotentHint: false
|
|
17192
|
+
},
|
|
17193
|
+
description: `${DESTRUCTIVE}changes a table of the project's D1 database without writing SQL, as one migration recorded in d1_migrations and written to ./migrations when the project directory is known. action is add_column (newColumn, shaped like a create_table column), rename_column (column, to), rename_table (to) or drop_column (column) \u2014 which deletes the column and its value in every row and needs confirm set to "<table>.<column>". SQLite cannot add a PRIMARY KEY or UNIQUE column, a NOT NULL column without a default that is not NULL, or a column defaulting to CURRENT_TIMESTAMP, and cannot drop a primary key, UNIQUE or indexed column: naro refuses those up front and says why. It also refuses to drop or rename a table or column that a trigger or view uses (read from their SQL; when the SQL leaves it open, a warning instead) \u2014 drop or recreate that trigger or view with apply_migration first. Changing a column's type is not an action \u2014 it needs a table rebuild, so write that migration with apply_migration. Data API policies are kept by table name and are not moved: read warnings after a rename or a drop. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
17194
|
+
inputSchema: {
|
|
17195
|
+
type: "object",
|
|
17196
|
+
properties: {
|
|
17197
|
+
...PROJECT_ARGS,
|
|
17198
|
+
table: { type: "string", description: "The table to change." },
|
|
17199
|
+
action: { type: "string", enum: [...ALTER_ACTIONS] },
|
|
17200
|
+
newColumn: {
|
|
17201
|
+
...COLUMN_SCHEMA,
|
|
17202
|
+
description: "add_column: the column to add, shaped like a create_table column."
|
|
17203
|
+
},
|
|
17204
|
+
column: {
|
|
17205
|
+
type: "string",
|
|
17206
|
+
description: "rename_column and drop_column: the column's name."
|
|
17207
|
+
},
|
|
17208
|
+
to: {
|
|
17209
|
+
type: "string",
|
|
17210
|
+
description: "rename_column and rename_table: the new name."
|
|
17211
|
+
},
|
|
17212
|
+
confirm: {
|
|
17213
|
+
type: "string",
|
|
17214
|
+
description: 'drop_column only: "<table>.<column>", spelled as in table and column.'
|
|
17215
|
+
},
|
|
17216
|
+
...SCHEMA_ARGS
|
|
17217
|
+
},
|
|
17218
|
+
required: ["table", "action"]
|
|
17219
|
+
},
|
|
17220
|
+
handler: async (args, deps) => {
|
|
17221
|
+
assertSchemaArgs(args);
|
|
17222
|
+
const action = args.action;
|
|
17223
|
+
if (typeof action !== "string" || !ALTER_ACTIONS.includes(action))
|
|
17224
|
+
throw new Error(`action must be one of ${ALTER_ACTIONS.join(", ")}.`);
|
|
17225
|
+
const table = required2(args.table, "table is required.");
|
|
17226
|
+
let operation;
|
|
17227
|
+
switch (action) {
|
|
17228
|
+
case "add_column":
|
|
17229
|
+
if (typeof args.newColumn !== "object" || args.newColumn === null)
|
|
17230
|
+
throw new Error("newColumn is required for add_column.");
|
|
17231
|
+
operation = {
|
|
17232
|
+
op: "addColumn",
|
|
17233
|
+
table,
|
|
17234
|
+
column: args.newColumn
|
|
17235
|
+
};
|
|
17236
|
+
break;
|
|
17237
|
+
case "rename_column": {
|
|
17238
|
+
const column = str(args.column);
|
|
17239
|
+
const to = str(args.to);
|
|
17240
|
+
if (!column || !to)
|
|
17241
|
+
throw new Error("column and to are required for rename_column.");
|
|
17242
|
+
operation = { op: "renameColumn", table, column, to };
|
|
17243
|
+
break;
|
|
17244
|
+
}
|
|
17245
|
+
case "drop_column":
|
|
17246
|
+
operation = {
|
|
17247
|
+
op: "dropColumn",
|
|
17248
|
+
table,
|
|
17249
|
+
column: required2(
|
|
17250
|
+
args.column,
|
|
17251
|
+
"column is required for drop_column."
|
|
17252
|
+
),
|
|
17253
|
+
// Forwarded as given; without it the server says what to pass.
|
|
17254
|
+
...args.confirm !== void 0 ? { confirm: args.confirm } : {}
|
|
17255
|
+
};
|
|
17256
|
+
break;
|
|
17257
|
+
case "rename_table":
|
|
17258
|
+
operation = {
|
|
17259
|
+
op: "renameTable",
|
|
17260
|
+
table,
|
|
17261
|
+
to: required2(args.to, "to is required for rename_table.")
|
|
17262
|
+
};
|
|
17263
|
+
break;
|
|
17264
|
+
}
|
|
17265
|
+
return runSchemaTool(args, deps, operation);
|
|
17266
|
+
}
|
|
17267
|
+
},
|
|
17268
|
+
{
|
|
17269
|
+
name: "drop_table",
|
|
17270
|
+
group: "database",
|
|
17271
|
+
untrusted: true,
|
|
17272
|
+
annotations: {
|
|
17273
|
+
readOnlyHint: false,
|
|
17274
|
+
destructiveHint: true,
|
|
17275
|
+
idempotentHint: false
|
|
17276
|
+
},
|
|
17277
|
+
description: `${DESTRUCTIVE}drops a table of the project's D1 database and every row in it, as one migration recorded in d1_migrations. Needs confirm set to the table's name. A table that another table's foreign key references, or that a trigger or view uses, is refused. restore_database can bring it back (Time Travel, 30 days). Its Data API policy is removed with it whenever the project has a backend, on or off (removedPolicies); otherwise it stays under the table's name: read warnings. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
17278
|
+
inputSchema: {
|
|
17279
|
+
type: "object",
|
|
17280
|
+
properties: {
|
|
17281
|
+
...PROJECT_ARGS,
|
|
17282
|
+
table: { type: "string", description: "The table to drop." },
|
|
17283
|
+
confirm: {
|
|
17284
|
+
type: "string",
|
|
17285
|
+
description: "The table's name, spelled as in table."
|
|
17286
|
+
},
|
|
17287
|
+
...SCHEMA_ARGS
|
|
17288
|
+
},
|
|
17289
|
+
required: ["table", "confirm"]
|
|
17290
|
+
},
|
|
17291
|
+
handler: async (args, deps) => {
|
|
17292
|
+
assertSchemaArgs(args);
|
|
17293
|
+
const table = required2(args.table, "table is required.");
|
|
17294
|
+
return runSchemaTool(args, deps, {
|
|
17295
|
+
op: "dropTable",
|
|
17296
|
+
table,
|
|
17297
|
+
...args.confirm !== void 0 ? { confirm: args.confirm } : {}
|
|
17298
|
+
});
|
|
17299
|
+
}
|
|
17300
|
+
},
|
|
17301
|
+
{
|
|
17302
|
+
name: "create_index",
|
|
17303
|
+
group: "database",
|
|
17304
|
+
untrusted: true,
|
|
17305
|
+
annotations: {
|
|
17306
|
+
readOnlyHint: false,
|
|
17307
|
+
destructiveHint: false,
|
|
17308
|
+
idempotentHint: false
|
|
17309
|
+
},
|
|
17310
|
+
description: `Creates an index on columns of a table in the project's D1 database, as one migration recorded in d1_migrations and written to ./migrations when the project directory is known. unique: true makes it a unique index \u2014 SQLite refuses it while the columns hold duplicates. name defaults to <table>_<columns>_idx. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
17311
|
+
inputSchema: {
|
|
17312
|
+
type: "object",
|
|
17313
|
+
properties: {
|
|
17314
|
+
...PROJECT_ARGS,
|
|
17315
|
+
table: { type: "string" },
|
|
17316
|
+
columns: {
|
|
17317
|
+
type: "array",
|
|
17318
|
+
items: { type: "string" },
|
|
17319
|
+
description: "1 to 16 column names, in index order."
|
|
17320
|
+
},
|
|
17321
|
+
unique: { type: "boolean" },
|
|
17322
|
+
name: {
|
|
17323
|
+
type: "string",
|
|
17324
|
+
description: "The index's name. Defaults to <table>_<columns>_idx."
|
|
17325
|
+
},
|
|
17326
|
+
...SCHEMA_ARGS
|
|
17327
|
+
},
|
|
17328
|
+
required: ["table", "columns"]
|
|
17329
|
+
},
|
|
17330
|
+
handler: async (args, deps) => {
|
|
17331
|
+
assertSchemaArgs(args);
|
|
17332
|
+
const table = required2(args.table, "table is required.");
|
|
17333
|
+
if (!Array.isArray(args.columns)) throw new Error("columns is required.");
|
|
17334
|
+
return runSchemaTool(args, deps, {
|
|
17335
|
+
op: "createIndex",
|
|
17336
|
+
table,
|
|
17337
|
+
columns: args.columns,
|
|
17338
|
+
...args.unique !== void 0 ? { unique: args.unique } : {},
|
|
17339
|
+
...args.name !== void 0 ? { name: args.name } : {}
|
|
17340
|
+
});
|
|
17341
|
+
}
|
|
17342
|
+
},
|
|
17343
|
+
{
|
|
17344
|
+
name: "drop_index",
|
|
17345
|
+
group: "database",
|
|
17346
|
+
untrusted: true,
|
|
17347
|
+
annotations: {
|
|
17348
|
+
readOnlyHint: false,
|
|
17349
|
+
destructiveHint: true,
|
|
17350
|
+
idempotentHint: false
|
|
17351
|
+
},
|
|
17352
|
+
description: `${DESTRUCTIVE}drops an index of the project's D1 database, as one migration recorded in d1_migrations. No rows are lost, but a unique index takes its constraint with it. The index SQLite made for a UNIQUE or PRIMARY KEY constraint (sqlite_autoindex_\u2026) cannot be dropped. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
17353
|
+
inputSchema: {
|
|
17354
|
+
type: "object",
|
|
17355
|
+
properties: {
|
|
17356
|
+
...PROJECT_ARGS,
|
|
17357
|
+
name: { type: "string", description: "The index to drop." },
|
|
17358
|
+
...SCHEMA_ARGS
|
|
17359
|
+
},
|
|
17360
|
+
required: ["name"]
|
|
17361
|
+
},
|
|
17362
|
+
handler: async (args, deps) => {
|
|
17363
|
+
assertSchemaArgs(args);
|
|
17364
|
+
return runSchemaTool(args, deps, {
|
|
17365
|
+
op: "dropIndex",
|
|
17366
|
+
name: required2(args.name, "name is required.")
|
|
17367
|
+
});
|
|
17368
|
+
}
|
|
17369
|
+
},
|
|
16413
17370
|
{
|
|
16414
17371
|
name: "list_migrations",
|
|
16415
17372
|
group: "database",
|
|
@@ -16440,7 +17397,7 @@ var DATABASE_TOOLS = [
|
|
|
16440
17397
|
destructiveHint: false,
|
|
16441
17398
|
idempotentHint: true
|
|
16442
17399
|
},
|
|
16443
|
-
description: `Generates TypeScript types for the D1 schema: one interface per table, a Database
|
|
17400
|
+
description: `Generates TypeScript types for the D1 schema: one Row interface per table and view, a Database type with Row, Insert and Update per table that createClient<Database>() from @naro/sdk reads, the Tables, TablesInsert and TablesUpdate helpers, and an Env with the DB binding. Returns the text only \u2014 write it to a file yourself (for example src/database.types.ts). A view that could not be described is an empty interface in the text and is named under the undescribed key. Where the backend's insert rule for a table is owner, its owner column is optional in Insert (the backend fills it); when the policies cannot be read, the result says policies: "unavailable" with policiesReason, and the owner columns stay required. ${UNTRUSTED}`,
|
|
16444
17401
|
inputSchema: { type: "object", properties: { ...PROJECT_ARGS } },
|
|
16445
17402
|
handler: async (args, deps) => {
|
|
16446
17403
|
assertProjectArgs(args);
|
|
@@ -16448,14 +17405,20 @@ var DATABASE_TOOLS = [
|
|
|
16448
17405
|
projectId: str(args.projectId),
|
|
16449
17406
|
directory: str(args.directory)
|
|
16450
17407
|
});
|
|
16451
|
-
const
|
|
17408
|
+
const client = await deps.client();
|
|
17409
|
+
const tables = await client.db.tables(projectId);
|
|
17410
|
+
const read = await policiesForTypes(client, projectId);
|
|
16452
17411
|
const undescribed = tables.flatMap(
|
|
16453
17412
|
(table) => table.error === void 0 ? [] : [{ name: table.name, error: table.error }]
|
|
16454
17413
|
);
|
|
16455
17414
|
return {
|
|
16456
17415
|
projectId,
|
|
16457
|
-
types: generateTypes(
|
|
16458
|
-
|
|
17416
|
+
types: generateTypes(
|
|
17417
|
+
tables,
|
|
17418
|
+
read.policies === void 0 ? {} : { policies: read.policies }
|
|
17419
|
+
),
|
|
17420
|
+
...undescribed.length > 0 ? { undescribed } : {},
|
|
17421
|
+
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
16459
17422
|
};
|
|
16460
17423
|
}
|
|
16461
17424
|
},
|
|
@@ -17376,7 +18339,7 @@ function toolErrorJson(error51) {
|
|
|
17376
18339
|
}
|
|
17377
18340
|
|
|
17378
18341
|
// src/server.ts
|
|
17379
|
-
var SERVER_VERSION = "0.
|
|
18342
|
+
var SERVER_VERSION = "0.9.0";
|
|
17380
18343
|
function visibleTools(options2) {
|
|
17381
18344
|
const { features, readOnly } = options2;
|
|
17382
18345
|
return TOOLS.flatMap((tool) => {
|