@noy-db/cli 0.1.0-pre.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/bin/noydb.cjs +673 -0
- package/dist/bin/noydb.cjs.map +1 -0
- package/dist/bin/noydb.js +650 -0
- package/dist/bin/noydb.js.map +1 -0
- package/dist/index.cjs +649 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +602 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
formatSnapshot: () => formatSnapshot,
|
|
34
|
+
inspect: () => inspect,
|
|
35
|
+
loadOptionsFromFile: () => loadOptionsFromFile,
|
|
36
|
+
runConfigScaffold: () => runConfigScaffold,
|
|
37
|
+
runConfigValidate: () => runConfigValidate,
|
|
38
|
+
runInspect: () => runInspect,
|
|
39
|
+
runMonitor: () => runMonitor,
|
|
40
|
+
runVerify: () => runVerify,
|
|
41
|
+
scaffold: () => scaffold,
|
|
42
|
+
validateOptions: () => validateOptions,
|
|
43
|
+
verify: () => verify
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(src_exports);
|
|
46
|
+
|
|
47
|
+
// src/commands/inspect.ts
|
|
48
|
+
var import_hub = require("@noy-db/hub");
|
|
49
|
+
var import_promises = require("fs/promises");
|
|
50
|
+
async function inspect(filePath) {
|
|
51
|
+
const bytes = await (0, import_promises.readFile)(filePath);
|
|
52
|
+
const header = (0, import_hub.readNoydbBundleHeader)(new Uint8Array(bytes));
|
|
53
|
+
return {
|
|
54
|
+
formatVersion: header.formatVersion,
|
|
55
|
+
handle: header.handle,
|
|
56
|
+
bodyBytes: header.bodyBytes,
|
|
57
|
+
bodySha256: header.bodySha256
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function runInspect(argv) {
|
|
61
|
+
const file = argv[0];
|
|
62
|
+
if (!file) {
|
|
63
|
+
process.stderr.write("usage: noydb inspect <file.noydb>\n");
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const info = await inspect(file);
|
|
68
|
+
process.stdout.write(JSON.stringify(info, null, 2) + "\n");
|
|
69
|
+
return 0;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
process.stderr.write(`inspect failed: ${err.message}
|
|
72
|
+
`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/commands/verify.ts
|
|
78
|
+
var import_hub2 = require("@noy-db/hub");
|
|
79
|
+
var import_promises2 = require("fs/promises");
|
|
80
|
+
async function verify(filePath) {
|
|
81
|
+
const bytes = new Uint8Array(await (0, import_promises2.readFile)(filePath));
|
|
82
|
+
const checks = { magic: false, header: false, bodyHash: false };
|
|
83
|
+
let handle = "";
|
|
84
|
+
let bodyBytes = 0;
|
|
85
|
+
try {
|
|
86
|
+
const header = (0, import_hub2.readNoydbBundleHeader)(bytes);
|
|
87
|
+
checks.magic = true;
|
|
88
|
+
checks.header = true;
|
|
89
|
+
handle = header.handle;
|
|
90
|
+
bodyBytes = header.bodyBytes;
|
|
91
|
+
await (0, import_hub2.readNoydbBundle)(bytes);
|
|
92
|
+
checks.bodyHash = true;
|
|
93
|
+
return { ok: true, file: filePath, handle, bodyBytes, checks };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
file: filePath,
|
|
98
|
+
handle,
|
|
99
|
+
bodyBytes,
|
|
100
|
+
checks,
|
|
101
|
+
error: err.message
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function runVerify(argv) {
|
|
106
|
+
const file = argv[0];
|
|
107
|
+
if (!file) {
|
|
108
|
+
process.stderr.write("usage: noydb verify <file.noydb>\n");
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
const report = await verify(file);
|
|
112
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
113
|
+
return report.ok ? 0 : 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/commands/config.ts
|
|
117
|
+
var import_node_url = require("url");
|
|
118
|
+
var import_node_path = require("path");
|
|
119
|
+
function safeStringify(v) {
|
|
120
|
+
if (typeof v === "string") return v;
|
|
121
|
+
if (typeof v === "number" || typeof v === "boolean" || v === null || v === void 0) {
|
|
122
|
+
return String(v);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return JSON.stringify(v);
|
|
126
|
+
} catch {
|
|
127
|
+
return "<unserializable>";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function validateOptions(opts) {
|
|
131
|
+
const issues = [];
|
|
132
|
+
if (!isRecord(opts)) {
|
|
133
|
+
issues.push({
|
|
134
|
+
severity: "error",
|
|
135
|
+
code: "not-object",
|
|
136
|
+
path: "<root>",
|
|
137
|
+
message: "NoydbOptions must be an object"
|
|
138
|
+
});
|
|
139
|
+
return { ok: false, issues };
|
|
140
|
+
}
|
|
141
|
+
if (!opts.store) {
|
|
142
|
+
issues.push({
|
|
143
|
+
severity: "error",
|
|
144
|
+
code: "missing-store",
|
|
145
|
+
path: "store",
|
|
146
|
+
message: "`store` is required"
|
|
147
|
+
});
|
|
148
|
+
} else if (!isStoreShape(opts.store)) {
|
|
149
|
+
issues.push({
|
|
150
|
+
severity: "error",
|
|
151
|
+
code: "bad-store-shape",
|
|
152
|
+
path: "store",
|
|
153
|
+
message: "`store` does not expose the 6-method NoydbStore contract"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (opts.sync !== void 0) {
|
|
157
|
+
const targets = normalizeSync(opts.sync);
|
|
158
|
+
targets.forEach((t, i) => validateTarget(t, `sync[${i}]`, issues));
|
|
159
|
+
}
|
|
160
|
+
if (opts.syncPolicy !== void 0 && opts.sync === void 0) {
|
|
161
|
+
issues.push({
|
|
162
|
+
severity: "warn",
|
|
163
|
+
code: "policy-without-sync",
|
|
164
|
+
path: "syncPolicy",
|
|
165
|
+
message: "`syncPolicy` has no effect without a `sync` target"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (typeof opts.user !== "string" || !opts.user) {
|
|
169
|
+
issues.push({
|
|
170
|
+
severity: "warn",
|
|
171
|
+
code: "missing-user",
|
|
172
|
+
path: "user",
|
|
173
|
+
message: '`user` identifier is recommended \u2014 audit entries default to "anonymous" without it'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (opts.secret === void 0 && opts.passphrase === void 0 && opts.encrypt !== false) {
|
|
177
|
+
issues.push({
|
|
178
|
+
severity: "warn",
|
|
179
|
+
code: "no-secret",
|
|
180
|
+
path: "secret",
|
|
181
|
+
message: "`secret` / `passphrase` missing and `encrypt` not explicitly false \u2014 vault open will fail"
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const hasError = issues.some((i) => i.severity === "error");
|
|
185
|
+
return { ok: !hasError, issues };
|
|
186
|
+
}
|
|
187
|
+
async function loadOptionsFromFile(filePath) {
|
|
188
|
+
const abs = (0, import_node_path.resolve)(filePath);
|
|
189
|
+
if (abs.endsWith(".ts") || abs.endsWith(".mts") || abs.endsWith(".cts")) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`TypeScript config files are not directly loadable \u2014 Node has no native .ts loader.
|
|
192
|
+
Options:
|
|
193
|
+
(a) compile first: tsc ${filePath} && noydb config validate ${filePath.replace(/\.[mc]?ts$/, ".js")}
|
|
194
|
+
(b) run via tsx: npx tsx $(which noydb) config validate ${filePath}
|
|
195
|
+
(c) rename to .mjs/.js if your config has no TS-only syntax`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const mod = await import((0, import_node_url.pathToFileURL)(abs).href);
|
|
199
|
+
const value = mod.default ?? mod;
|
|
200
|
+
if (typeof value === "function") {
|
|
201
|
+
return await value();
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
async function runConfigValidate(argv) {
|
|
206
|
+
const file = argv[0];
|
|
207
|
+
if (!file) {
|
|
208
|
+
process.stderr.write("usage: noydb config validate <file.ts|js>\n");
|
|
209
|
+
return 2;
|
|
210
|
+
}
|
|
211
|
+
let opts;
|
|
212
|
+
try {
|
|
213
|
+
opts = await loadOptionsFromFile(file);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
process.stderr.write(`failed to load ${file}: ${err.message}
|
|
216
|
+
`);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
const report = validateOptions(opts);
|
|
220
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
221
|
+
return report.ok ? 0 : 1;
|
|
222
|
+
}
|
|
223
|
+
function scaffold(profile) {
|
|
224
|
+
switch (profile) {
|
|
225
|
+
case "A":
|
|
226
|
+
return {
|
|
227
|
+
profile,
|
|
228
|
+
notes: "Local-only single-user. No cloud.",
|
|
229
|
+
code: [
|
|
230
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
231
|
+
`import { jsonFile } from '@noy-db/to-file'`,
|
|
232
|
+
``,
|
|
233
|
+
`export default {`,
|
|
234
|
+
` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? './data' }),`,
|
|
235
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
236
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
237
|
+
`}`
|
|
238
|
+
].join("\n"),
|
|
239
|
+
env: `NOYDB_USER=owner
|
|
240
|
+
NOYDB_SECRET=
|
|
241
|
+
NOYDB_DATA_DIR=./data
|
|
242
|
+
`
|
|
243
|
+
};
|
|
244
|
+
case "B":
|
|
245
|
+
return {
|
|
246
|
+
profile,
|
|
247
|
+
notes: "Offline-first + cloud mirror. Local authoritative, sync opportunistically.",
|
|
248
|
+
code: [
|
|
249
|
+
`import { createNoydb, INDEXED_STORE_POLICY } from '@noy-db/hub'`,
|
|
250
|
+
`import { browserIdbStore } from '@noy-db/to-browser-idb'`,
|
|
251
|
+
`import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
|
|
252
|
+
``,
|
|
253
|
+
`export default {`,
|
|
254
|
+
` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'myapp' }),`,
|
|
255
|
+
` sync: [{`,
|
|
256
|
+
` store: awsDynamoStore({`,
|
|
257
|
+
` table: process.env.NOYDB_DYNAMO_TABLE!,`,
|
|
258
|
+
` region: process.env.AWS_REGION ?? 'us-east-1',`,
|
|
259
|
+
` }),`,
|
|
260
|
+
` role: 'sync-peer',`,
|
|
261
|
+
` label: 'dynamo-live',`,
|
|
262
|
+
` }],`,
|
|
263
|
+
` syncPolicy: INDEXED_STORE_POLICY,`,
|
|
264
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
265
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
266
|
+
`}`
|
|
267
|
+
].join("\n"),
|
|
268
|
+
env: `NOYDB_USER=owner
|
|
269
|
+
NOYDB_SECRET=
|
|
270
|
+
NOYDB_APP=myapp
|
|
271
|
+
NOYDB_DYNAMO_TABLE=myapp-live
|
|
272
|
+
AWS_REGION=us-east-1
|
|
273
|
+
`
|
|
274
|
+
};
|
|
275
|
+
case "C":
|
|
276
|
+
return {
|
|
277
|
+
profile,
|
|
278
|
+
notes: "Records + blobs split (routeStore). Dynamo for records, S3 for blobs.",
|
|
279
|
+
code: [
|
|
280
|
+
`import { createNoydb, routeStore } from '@noy-db/hub'`,
|
|
281
|
+
`import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
|
|
282
|
+
`import { awsS3Store } from '@noy-db/to-aws-s3'`,
|
|
283
|
+
``,
|
|
284
|
+
`export default {`,
|
|
285
|
+
` store: routeStore({`,
|
|
286
|
+
` default: awsDynamoStore({`,
|
|
287
|
+
` table: process.env.NOYDB_DYNAMO_TABLE!,`,
|
|
288
|
+
` region: process.env.AWS_REGION ?? 'us-east-1',`,
|
|
289
|
+
` }),`,
|
|
290
|
+
` blobs: awsS3Store({`,
|
|
291
|
+
` bucket: process.env.NOYDB_S3_BUCKET!,`,
|
|
292
|
+
` region: process.env.AWS_REGION ?? 'us-east-1',`,
|
|
293
|
+
` }),`,
|
|
294
|
+
` }),`,
|
|
295
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
296
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
297
|
+
`}`
|
|
298
|
+
].join("\n"),
|
|
299
|
+
env: `NOYDB_USER=owner
|
|
300
|
+
NOYDB_SECRET=
|
|
301
|
+
NOYDB_DYNAMO_TABLE=myapp-records
|
|
302
|
+
NOYDB_S3_BUCKET=myapp-blobs
|
|
303
|
+
AWS_REGION=us-east-1
|
|
304
|
+
`
|
|
305
|
+
};
|
|
306
|
+
case "G":
|
|
307
|
+
return {
|
|
308
|
+
profile,
|
|
309
|
+
notes: "Middleware-hardened production: retry + breaker + cache + metrics.",
|
|
310
|
+
code: [
|
|
311
|
+
`import { createNoydb, wrapStore, withRetry, withCircuitBreaker, withCache, withHealthCheck, withMetrics } from '@noy-db/hub'`,
|
|
312
|
+
`import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
|
|
313
|
+
``,
|
|
314
|
+
`export default {`,
|
|
315
|
+
` store: wrapStore(`,
|
|
316
|
+
` awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
|
|
317
|
+
` withRetry({ maxRetries: 3 }),`,
|
|
318
|
+
` withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 }),`,
|
|
319
|
+
` withCache({ ttlMs: 60_000 }),`,
|
|
320
|
+
` withHealthCheck(),`,
|
|
321
|
+
` withMetrics({ onOperation: op => console.log(op) }),`,
|
|
322
|
+
` ),`,
|
|
323
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
324
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
325
|
+
`}`
|
|
326
|
+
].join("\n"),
|
|
327
|
+
env: `NOYDB_USER=owner
|
|
328
|
+
NOYDB_SECRET=
|
|
329
|
+
NOYDB_DYNAMO_TABLE=myapp-prod
|
|
330
|
+
`
|
|
331
|
+
};
|
|
332
|
+
case "D":
|
|
333
|
+
return {
|
|
334
|
+
profile,
|
|
335
|
+
notes: "Hot + cold tiered. Records age out to archive after N days.",
|
|
336
|
+
code: [
|
|
337
|
+
`import { createNoydb, routeStore } from '@noy-db/hub'`,
|
|
338
|
+
`import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
|
|
339
|
+
`import { awsS3Store } from '@noy-db/to-aws-s3'`,
|
|
340
|
+
``,
|
|
341
|
+
`export default {`,
|
|
342
|
+
` store: routeStore({`,
|
|
343
|
+
` default: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
|
|
344
|
+
` age: {`,
|
|
345
|
+
` cold: awsS3Store({ bucket: process.env.NOYDB_S3_COLD! }),`,
|
|
346
|
+
` coldAfterDays: Number(process.env.NOYDB_COLD_AFTER_DAYS ?? '90'),`,
|
|
347
|
+
` },`,
|
|
348
|
+
` }),`,
|
|
349
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
350
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
351
|
+
`}`
|
|
352
|
+
].join("\n"),
|
|
353
|
+
env: `NOYDB_USER=owner
|
|
354
|
+
NOYDB_SECRET=
|
|
355
|
+
NOYDB_DYNAMO_TABLE=myapp-hot
|
|
356
|
+
NOYDB_S3_COLD=myapp-archive
|
|
357
|
+
NOYDB_COLD_AFTER_DAYS=90
|
|
358
|
+
`
|
|
359
|
+
};
|
|
360
|
+
case "E":
|
|
361
|
+
return {
|
|
362
|
+
profile,
|
|
363
|
+
notes: "Multi-peer team sync. Primary + peer + backup + archive.",
|
|
364
|
+
code: [
|
|
365
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
366
|
+
`import { browserIdbStore } from '@noy-db/to-browser-idb'`,
|
|
367
|
+
`import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
|
|
368
|
+
`import { awsS3Store } from '@noy-db/to-aws-s3'`,
|
|
369
|
+
``,
|
|
370
|
+
`export default {`,
|
|
371
|
+
` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'team' }),`,
|
|
372
|
+
` sync: [`,
|
|
373
|
+
` { store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer', label: 'team-hot' },`,
|
|
374
|
+
` { store: awsS3Store({ bucket: process.env.NOYDB_S3_BACKUP! }), role: 'backup', label: 'team-backup' },`,
|
|
375
|
+
` { store: awsS3Store({ bucket: process.env.NOYDB_S3_ARCHIVE! }), role: 'archive', label: 'team-archive' },`,
|
|
376
|
+
` ],`,
|
|
377
|
+
` user: process.env.NOYDB_USER ?? 'member',`,
|
|
378
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
379
|
+
`}`
|
|
380
|
+
].join("\n"),
|
|
381
|
+
env: `NOYDB_USER=member
|
|
382
|
+
NOYDB_SECRET=
|
|
383
|
+
NOYDB_APP=team
|
|
384
|
+
NOYDB_DYNAMO_TABLE=team-hot
|
|
385
|
+
NOYDB_S3_BACKUP=team-backup
|
|
386
|
+
NOYDB_S3_ARCHIVE=team-archive
|
|
387
|
+
`
|
|
388
|
+
};
|
|
389
|
+
case "F":
|
|
390
|
+
return {
|
|
391
|
+
profile,
|
|
392
|
+
notes: "CRDT collaboration \u2014 Yjs-backed shared records over the encrypted envelope.",
|
|
393
|
+
code: [
|
|
394
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
395
|
+
`import { browserIdbStore } from '@noy-db/to-browser-idb'`,
|
|
396
|
+
`import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
|
|
397
|
+
`// import { yjsCollection } from '@noy-db/in-yjs' // use inside your app`,
|
|
398
|
+
``,
|
|
399
|
+
`export default {`,
|
|
400
|
+
` store: browserIdbStore({ prefix: 'collab' }),`,
|
|
401
|
+
` sync: [{ store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer' }],`,
|
|
402
|
+
` user: process.env.NOYDB_USER!,`,
|
|
403
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
404
|
+
`}`,
|
|
405
|
+
``,
|
|
406
|
+
`// After createNoydb(), replace normal collections with yjsCollection() for CRDT fields.`
|
|
407
|
+
].join("\n"),
|
|
408
|
+
env: `NOYDB_USER=
|
|
409
|
+
NOYDB_SECRET=
|
|
410
|
+
NOYDB_DYNAMO_TABLE=collab-live
|
|
411
|
+
`
|
|
412
|
+
};
|
|
413
|
+
case "H":
|
|
414
|
+
return {
|
|
415
|
+
profile,
|
|
416
|
+
notes: "USB-portable \u2014 everything on a single file store, no cloud.",
|
|
417
|
+
code: [
|
|
418
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
419
|
+
`import { jsonFile } from '@noy-db/to-file'`,
|
|
420
|
+
``,
|
|
421
|
+
`export default {`,
|
|
422
|
+
` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? '/Volumes/MY_USB/data' }),`,
|
|
423
|
+
` user: process.env.NOYDB_USER!,`,
|
|
424
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
425
|
+
`}`
|
|
426
|
+
].join("\n"),
|
|
427
|
+
env: `NOYDB_USER=
|
|
428
|
+
NOYDB_SECRET=
|
|
429
|
+
NOYDB_DATA_DIR=/Volumes/MY_USB/data
|
|
430
|
+
`
|
|
431
|
+
};
|
|
432
|
+
case "I":
|
|
433
|
+
return {
|
|
434
|
+
profile,
|
|
435
|
+
notes: "Multi-tenant geographic sharding \u2014 per-vault primary per region.",
|
|
436
|
+
code: [
|
|
437
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
438
|
+
`import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
|
|
439
|
+
``,
|
|
440
|
+
`// Pick the nearest region at init time based on tenant config.`,
|
|
441
|
+
`const region = process.env.NOYDB_REGION ?? 'ap-southeast-1'`,
|
|
442
|
+
`const table = \`myapp-\${region}\``,
|
|
443
|
+
``,
|
|
444
|
+
`export default {`,
|
|
445
|
+
` store: awsDynamoStore({ table, region }),`,
|
|
446
|
+
` user: process.env.NOYDB_USER!,`,
|
|
447
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
448
|
+
`}`
|
|
449
|
+
].join("\n"),
|
|
450
|
+
env: `NOYDB_USER=
|
|
451
|
+
NOYDB_SECRET=
|
|
452
|
+
NOYDB_REGION=ap-southeast-1
|
|
453
|
+
`
|
|
454
|
+
};
|
|
455
|
+
case "J":
|
|
456
|
+
return {
|
|
457
|
+
profile,
|
|
458
|
+
notes: "Authentication bridge (passphrase-less unlock via OIDC or WebAuthn).",
|
|
459
|
+
code: [
|
|
460
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
461
|
+
`import { browserIdbStore } from '@noy-db/to-browser-idb'`,
|
|
462
|
+
`// import { unlockWebAuthn } from '@noy-db/on-webauthn' // in the browser`,
|
|
463
|
+
`// import { keyConnector } from '@noy-db/on-oidc' // in a server app`,
|
|
464
|
+
``,
|
|
465
|
+
`export default {`,
|
|
466
|
+
` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'app' }),`,
|
|
467
|
+
` user: process.env.NOYDB_USER!,`,
|
|
468
|
+
` // secret supplied by the unlock method at openVault() time, not here.`,
|
|
469
|
+
`}`
|
|
470
|
+
].join("\n"),
|
|
471
|
+
env: `NOYDB_USER=
|
|
472
|
+
NOYDB_APP=app
|
|
473
|
+
`
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function runConfigScaffold(argv) {
|
|
478
|
+
const profileArg = argv.find((a) => a.startsWith("--profile="));
|
|
479
|
+
const profile = profileArg?.split("=")[1] ?? "A";
|
|
480
|
+
if (!/^[A-J]$/.test(profile)) {
|
|
481
|
+
process.stderr.write(`unknown profile: ${profile}. Valid: A-J (see docs/guides/topology-matrix.md)
|
|
482
|
+
`);
|
|
483
|
+
return 2;
|
|
484
|
+
}
|
|
485
|
+
const out = scaffold(profile);
|
|
486
|
+
process.stdout.write(`// \u2500\u2500 noydb config (profile ${out.profile}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
487
|
+
`);
|
|
488
|
+
process.stdout.write(`// ${out.notes}
|
|
489
|
+
|
|
490
|
+
`);
|
|
491
|
+
process.stdout.write(out.code + "\n\n");
|
|
492
|
+
if (out.env) {
|
|
493
|
+
process.stdout.write(`// \u2500\u2500 .env template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
494
|
+
`);
|
|
495
|
+
process.stdout.write(out.env);
|
|
496
|
+
}
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
function isRecord(v) {
|
|
500
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
501
|
+
}
|
|
502
|
+
function isStoreShape(v) {
|
|
503
|
+
if (!isRecord(v)) return false;
|
|
504
|
+
return ["get", "put", "delete", "list", "loadAll", "saveAll"].every((m) => typeof v[m] === "function");
|
|
505
|
+
}
|
|
506
|
+
function normalizeSync(v) {
|
|
507
|
+
if (Array.isArray(v)) return v;
|
|
508
|
+
return [v];
|
|
509
|
+
}
|
|
510
|
+
function validateTarget(t, path, issues) {
|
|
511
|
+
if (!isRecord(t)) {
|
|
512
|
+
issues.push({
|
|
513
|
+
severity: "error",
|
|
514
|
+
code: "bad-target",
|
|
515
|
+
path,
|
|
516
|
+
message: "sync target must be an object with { store, role }"
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (t["role"] === void 0) {
|
|
521
|
+
if (!isStoreShape(t)) {
|
|
522
|
+
issues.push({
|
|
523
|
+
severity: "error",
|
|
524
|
+
code: "bad-store-shape",
|
|
525
|
+
path,
|
|
526
|
+
message: "sync target store does not expose the 6-method contract"
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const validRoles = ["sync-peer", "backup", "archive"];
|
|
532
|
+
if (!validRoles.includes(safeStringify(t["role"]))) {
|
|
533
|
+
issues.push({
|
|
534
|
+
severity: "error",
|
|
535
|
+
code: "bad-role",
|
|
536
|
+
path: `${path}.role`,
|
|
537
|
+
message: `role must be one of ${validRoles.join(", ")}; got ${safeStringify(t["role"])}`
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (!t["store"] || !isStoreShape(t["store"])) {
|
|
541
|
+
issues.push({
|
|
542
|
+
severity: "error",
|
|
543
|
+
code: "bad-store-shape",
|
|
544
|
+
path: `${path}.store`,
|
|
545
|
+
message: "sync target store does not expose the 6-method contract"
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (t["role"] === "archive" && isRecord(t["policy"]) && isRecord(t["policy"]["pull"])) {
|
|
549
|
+
issues.push({
|
|
550
|
+
severity: "error",
|
|
551
|
+
code: "archive-pull-configured",
|
|
552
|
+
path: `${path}.policy.pull`,
|
|
553
|
+
message: "archive targets are push-only \u2014 pull policy is invalid"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/commands/monitor.ts
|
|
559
|
+
var import_to_meter = require("@noy-db/to-meter");
|
|
560
|
+
async function runMonitor(argv) {
|
|
561
|
+
const file = argv[0];
|
|
562
|
+
if (!file) {
|
|
563
|
+
process.stderr.write("usage: noydb monitor <config.ts> [--interval=ms]\n");
|
|
564
|
+
return 2;
|
|
565
|
+
}
|
|
566
|
+
const intervalArg = argv.find((a) => a.startsWith("--interval="));
|
|
567
|
+
const intervalMs = intervalArg ? parseInt(intervalArg.split("=")[1] ?? "5000", 10) : 5e3;
|
|
568
|
+
let opts;
|
|
569
|
+
try {
|
|
570
|
+
const loaded = await loadOptionsFromFile(file);
|
|
571
|
+
if (typeof loaded !== "object" || loaded === null) {
|
|
572
|
+
process.stderr.write(`config file must export a NoydbOptions-shaped object
|
|
573
|
+
`);
|
|
574
|
+
return 1;
|
|
575
|
+
}
|
|
576
|
+
opts = loaded;
|
|
577
|
+
} catch (err) {
|
|
578
|
+
process.stderr.write(`failed to load ${file}: ${err.message}
|
|
579
|
+
`);
|
|
580
|
+
return 1;
|
|
581
|
+
}
|
|
582
|
+
const innerStore = opts["store"];
|
|
583
|
+
if (!innerStore || typeof innerStore !== "object") {
|
|
584
|
+
process.stderr.write("config has no `store` \u2014 nothing to monitor\n");
|
|
585
|
+
return 1;
|
|
586
|
+
}
|
|
587
|
+
const { store: metered, meter } = (0, import_to_meter.toMeter)(innerStore, {
|
|
588
|
+
degradedMs: 500,
|
|
589
|
+
onDegraded: (e) => process.stderr.write(`DEGRADED: ${e.reason}
|
|
590
|
+
`),
|
|
591
|
+
onRestored: (e) => process.stderr.write(`RESTORED: ${e.reason}
|
|
592
|
+
`)
|
|
593
|
+
});
|
|
594
|
+
const liveOpts = { ...opts, store: metered };
|
|
595
|
+
const hub = await import("@noy-db/hub");
|
|
596
|
+
await hub.createNoydb(liveOpts);
|
|
597
|
+
process.stdout.write(`monitoring ${file} \u2014 interval ${intervalMs}ms \u2014 Ctrl-C to stop
|
|
598
|
+
|
|
599
|
+
`);
|
|
600
|
+
const stop = installSigintHandler(meter);
|
|
601
|
+
return new Promise((resolveP) => {
|
|
602
|
+
const timer = setInterval(() => {
|
|
603
|
+
if (stop.signalled) {
|
|
604
|
+
clearInterval(timer);
|
|
605
|
+
meter.close();
|
|
606
|
+
resolveP(0);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const snap = meter.snapshot();
|
|
610
|
+
process.stdout.write(formatSnapshot(snap) + "\n");
|
|
611
|
+
}, intervalMs);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
function installSigintHandler(meter) {
|
|
615
|
+
const state = { signalled: false };
|
|
616
|
+
const handler = () => {
|
|
617
|
+
state.signalled = true;
|
|
618
|
+
meter.close();
|
|
619
|
+
};
|
|
620
|
+
process.on("SIGINT", handler);
|
|
621
|
+
process.on("SIGTERM", handler);
|
|
622
|
+
return state;
|
|
623
|
+
}
|
|
624
|
+
function formatSnapshot(snap) {
|
|
625
|
+
const lines = [];
|
|
626
|
+
const ts = new Date(snap.collectedAt).toISOString().slice(11, 19);
|
|
627
|
+
lines.push(`[${ts}] status=${snap.status} calls=${snap.totalCalls} casConflicts=${snap.casConflicts} windowMs=${snap.windowMs}`);
|
|
628
|
+
for (const m of ["get", "put", "delete", "list", "loadAll", "saveAll"]) {
|
|
629
|
+
const s = snap.byMethod[m];
|
|
630
|
+
if (s.count === 0) continue;
|
|
631
|
+
lines.push(` ${m.padEnd(7)} count=${s.count} errors=${s.errors} p50=${s.p50}ms p99=${s.p99}ms max=${s.max}ms avg=${s.avg}ms`);
|
|
632
|
+
}
|
|
633
|
+
return lines.join("\n");
|
|
634
|
+
}
|
|
635
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
636
|
+
0 && (module.exports = {
|
|
637
|
+
formatSnapshot,
|
|
638
|
+
inspect,
|
|
639
|
+
loadOptionsFromFile,
|
|
640
|
+
runConfigScaffold,
|
|
641
|
+
runConfigValidate,
|
|
642
|
+
runInspect,
|
|
643
|
+
runMonitor,
|
|
644
|
+
runVerify,
|
|
645
|
+
scaffold,
|
|
646
|
+
validateOptions,
|
|
647
|
+
verify
|
|
648
|
+
});
|
|
649
|
+
//# sourceMappingURL=index.cjs.map
|