@shaferllc/keel 0.68.0 → 0.74.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/AGENTS.md +2 -0
- package/README.md +14 -5
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.js +6 -0
- package/dist/core/cache.d.ts +1 -2
- package/dist/core/cache.js +9 -2
- package/dist/core/cli/stubs.d.ts +14 -0
- package/dist/core/cli/stubs.js +105 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/health.d.ts +2 -2
- package/dist/core/health.js +2 -2
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +25 -4
- package/dist/core/index.js +13 -3
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/logger.d.ts +7 -0
- package/dist/core/logger.js +28 -1
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.js +26 -5
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.js +15 -3
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +737 -3
- package/docs/api-resources.md +118 -0
- package/docs/configuration.md +74 -0
- package/docs/console.md +193 -13
- package/docs/database.md +101 -0
- package/docs/examples/configuration.ts +40 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/database.ts +90 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/logger.ts +74 -0
- package/docs/examples/mail.ts +103 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/testing.ts +149 -0
- package/docs/health.md +4 -4
- package/docs/i18n.md +302 -0
- package/docs/logger.md +156 -10
- package/docs/mail.md +73 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/storage.md +5 -3
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +3 -4
- package/docs/testing.md +251 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +2370 -248
- package/llms.txt +12 -1
- package/package.json +18 -2
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console — a real command system, not a wrapper round `process.argv`.
|
|
3
|
+
*
|
|
4
|
+
* export const greet = defineCommand({
|
|
5
|
+
* name: "greet",
|
|
6
|
+
* description: "Greet someone",
|
|
7
|
+
* args: { name: arg.string({ description: "who to greet" }) },
|
|
8
|
+
* flags: { loud: flag.boolean({ alias: "l", description: "SHOUT IT" }) },
|
|
9
|
+
*
|
|
10
|
+
* async run({ args, flags, ui }) {
|
|
11
|
+
* const message = `Hello, ${args.name}!`;
|
|
12
|
+
* ui.success(flags.loud ? message.toUpperCase() : message);
|
|
13
|
+
* },
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* `args.name` is a `string` and `flags.loud` is a `boolean` — **inferred from the
|
|
17
|
+
* spec**, not cast. Declare an arg optional and its type becomes `string |
|
|
18
|
+
* undefined`; give it a default and it's a `string` again. The types can't drift
|
|
19
|
+
* from the parsing, because the parsing is generated from them.
|
|
20
|
+
*
|
|
21
|
+
* A command gets a `ui` (colors, tables, tasks, spinners) and a `prompt` (ask,
|
|
22
|
+
* confirm, choice…), both of which switch to a captured, colorless "raw" mode
|
|
23
|
+
* under test, so you can assert on what a command printed and script its answers.
|
|
24
|
+
*/
|
|
25
|
+
/** Positional arguments: `keel greet <name>`. */
|
|
26
|
+
export const arg = {
|
|
27
|
+
string(options) {
|
|
28
|
+
return { kind: "arg", type: "string", required: true, ...options };
|
|
29
|
+
},
|
|
30
|
+
number(options) {
|
|
31
|
+
return { kind: "arg", type: "number", required: true, ...options };
|
|
32
|
+
},
|
|
33
|
+
/** Swallows every remaining value. Must be the last argument. */
|
|
34
|
+
spread(options) {
|
|
35
|
+
return { kind: "arg", type: "spread", required: true, ...options };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
/** Named options: `--force`, `-f`, `--name=Ada`. */
|
|
39
|
+
export const flag = {
|
|
40
|
+
/** `--force` / `-f`, and `--no-force`. Defaults to `false`, so it's never undefined. */
|
|
41
|
+
boolean(options) {
|
|
42
|
+
return { kind: "flag", type: "boolean", required: false, default: false, ...options };
|
|
43
|
+
},
|
|
44
|
+
string(options) {
|
|
45
|
+
return { kind: "flag", type: "string", required: false, ...options };
|
|
46
|
+
},
|
|
47
|
+
number(options) {
|
|
48
|
+
return { kind: "flag", type: "number", required: false, ...options };
|
|
49
|
+
},
|
|
50
|
+
/** Repeatable: `--tag a --tag b` gives `["a", "b"]`. Defaults to `[]`. */
|
|
51
|
+
array(options) {
|
|
52
|
+
return { kind: "flag", type: "array", required: false, default: [], ...options };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Define a command. The `args` and `flags` you declare become the types of
|
|
57
|
+
* `args` and `flags` inside `run` — the whole point of the exercise.
|
|
58
|
+
*/
|
|
59
|
+
export function defineCommand(definition) {
|
|
60
|
+
return definition;
|
|
61
|
+
}
|
|
62
|
+
/** Thrown when the argv doesn't fit the command's spec. */
|
|
63
|
+
export class ConsoleError extends Error {
|
|
64
|
+
constructor(message) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "ConsoleError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/* --------------------------------- parsing -------------------------------- */
|
|
70
|
+
function coerce(spec, raw, label) {
|
|
71
|
+
if (spec.parse)
|
|
72
|
+
return spec.parse(raw);
|
|
73
|
+
if (spec.type === "number") {
|
|
74
|
+
const value = Number(raw);
|
|
75
|
+
if (Number.isNaN(value))
|
|
76
|
+
throw new ConsoleError(`${label} expects a number, got "${raw}".`);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Turn argv into a command's args and flags.
|
|
83
|
+
*
|
|
84
|
+
* Handles `--flag value`, `--flag=value`, `--no-flag`, `-f value`, bundled short
|
|
85
|
+
* flags (`-abc`), repeatable array flags, a `--` terminator, and a trailing
|
|
86
|
+
* spread argument.
|
|
87
|
+
*/
|
|
88
|
+
export function parseArgv(argv, definition) {
|
|
89
|
+
const argSpecs = Object.entries(definition.args ?? {});
|
|
90
|
+
const flagSpecs = Object.entries(definition.flags ?? {});
|
|
91
|
+
const byAlias = new Map();
|
|
92
|
+
for (const [name, spec] of flagSpecs)
|
|
93
|
+
if (spec.alias)
|
|
94
|
+
byAlias.set(spec.alias, name);
|
|
95
|
+
const flags = {};
|
|
96
|
+
const positional = [];
|
|
97
|
+
const rest = [];
|
|
98
|
+
const specFor = (token) => {
|
|
99
|
+
const name = byAlias.get(token) ?? token;
|
|
100
|
+
const spec = definition.flags?.[name];
|
|
101
|
+
return spec ? [name, spec] : undefined;
|
|
102
|
+
};
|
|
103
|
+
const setFlag = (name, spec, raw) => {
|
|
104
|
+
const label = `--${name}`;
|
|
105
|
+
if (spec.type === "array") {
|
|
106
|
+
const list = flags[name] ?? [];
|
|
107
|
+
list.push(String(coerce(spec, raw, label)));
|
|
108
|
+
flags[name] = list;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
flags[name] = coerce(spec, raw, label);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
for (let i = 0; i < argv.length; i++) {
|
|
115
|
+
const token = argv[i];
|
|
116
|
+
// Everything after `--` is verbatim.
|
|
117
|
+
if (token === "--") {
|
|
118
|
+
rest.push(...argv.slice(i + 1));
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (token.startsWith("--")) {
|
|
122
|
+
const body = token.slice(2);
|
|
123
|
+
const eq = body.indexOf("=");
|
|
124
|
+
const name = eq === -1 ? body : body.slice(0, eq);
|
|
125
|
+
const inline = eq === -1 ? undefined : body.slice(eq + 1);
|
|
126
|
+
// `--no-force` turns a boolean off.
|
|
127
|
+
if (name.startsWith("no-")) {
|
|
128
|
+
const found = specFor(name.slice(3));
|
|
129
|
+
if (found && found[1].type === "boolean") {
|
|
130
|
+
flags[found[0]] = false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const found = specFor(name);
|
|
135
|
+
if (!found) {
|
|
136
|
+
if (definition.allowUnknownFlags) {
|
|
137
|
+
rest.push(token);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
throw new ConsoleError(`Unknown flag "--${name}".`);
|
|
141
|
+
}
|
|
142
|
+
const [key, spec] = found;
|
|
143
|
+
if (spec.type === "boolean") {
|
|
144
|
+
flags[key] = inline === undefined ? true : inline !== "false";
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const value = inline ?? argv[++i];
|
|
148
|
+
if (value === undefined)
|
|
149
|
+
throw new ConsoleError(`Flag "--${key}" expects a value.`);
|
|
150
|
+
setFlag(key, spec, value);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// `-f`, or bundled `-abc` where only the last may take a value.
|
|
154
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
155
|
+
const letters = [...token.slice(1)];
|
|
156
|
+
for (let j = 0; j < letters.length; j++) {
|
|
157
|
+
const letter = letters[j];
|
|
158
|
+
const found = specFor(letter);
|
|
159
|
+
if (!found) {
|
|
160
|
+
if (definition.allowUnknownFlags) {
|
|
161
|
+
rest.push(`-${letter}`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
throw new ConsoleError(`Unknown flag "-${letter}".`);
|
|
165
|
+
}
|
|
166
|
+
const [key, spec] = found;
|
|
167
|
+
if (spec.type === "boolean") {
|
|
168
|
+
flags[key] = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// A value-taking flag must be last in the bundle.
|
|
172
|
+
const value = j === letters.length - 1 ? argv[++i] : letters.slice(j + 1).join("");
|
|
173
|
+
if (value === undefined)
|
|
174
|
+
throw new ConsoleError(`Flag "-${letter}" expects a value.`);
|
|
175
|
+
setFlag(key, spec, value);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
positional.push(token);
|
|
181
|
+
}
|
|
182
|
+
// Positionals -> declared args.
|
|
183
|
+
const args = {};
|
|
184
|
+
let cursor = 0;
|
|
185
|
+
for (const [name, spec] of argSpecs) {
|
|
186
|
+
if (spec.type === "spread") {
|
|
187
|
+
const values = positional.slice(cursor);
|
|
188
|
+
cursor = positional.length;
|
|
189
|
+
if (!values.length && spec.required && spec.default === undefined) {
|
|
190
|
+
throw new ConsoleError(`Missing argument "${name}".`);
|
|
191
|
+
}
|
|
192
|
+
args[name] = values.length ? values : (spec.default ?? []);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const raw = positional[cursor++];
|
|
196
|
+
if (raw === undefined) {
|
|
197
|
+
if (spec.default !== undefined) {
|
|
198
|
+
args[name] = spec.default;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (spec.required)
|
|
202
|
+
throw new ConsoleError(`Missing argument "${name}".`);
|
|
203
|
+
args[name] = undefined;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
args[name] = coerce(spec, raw, `Argument "${name}"`);
|
|
207
|
+
}
|
|
208
|
+
// Leftover positionals nobody claimed.
|
|
209
|
+
rest.push(...positional.slice(cursor));
|
|
210
|
+
// Defaults, then required checks.
|
|
211
|
+
for (const [name, spec] of flagSpecs) {
|
|
212
|
+
if (flags[name] === undefined && spec.default !== undefined)
|
|
213
|
+
flags[name] = spec.default;
|
|
214
|
+
if (flags[name] === undefined && spec.required) {
|
|
215
|
+
throw new ConsoleError(`Missing required flag "--${name}".`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { args, flags, rest };
|
|
219
|
+
}
|
|
220
|
+
/** Registers commands, parses argv, and runs the right one. */
|
|
221
|
+
export class ConsoleKernel {
|
|
222
|
+
options;
|
|
223
|
+
commands = new Map();
|
|
224
|
+
aliases = new Map();
|
|
225
|
+
constructor(options = {}) {
|
|
226
|
+
this.options = options;
|
|
227
|
+
}
|
|
228
|
+
get binary() {
|
|
229
|
+
return this.options.binary ?? "keel";
|
|
230
|
+
}
|
|
231
|
+
/** Register one or more commands. */
|
|
232
|
+
register(...commands) {
|
|
233
|
+
for (const command of commands) {
|
|
234
|
+
this.commands.set(command.name, command);
|
|
235
|
+
for (const alias of command.aliases ?? [])
|
|
236
|
+
this.aliases.set(alias, command.name);
|
|
237
|
+
}
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
/** Every registered command, sorted by name. */
|
|
241
|
+
list() {
|
|
242
|
+
return [...this.commands.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
243
|
+
}
|
|
244
|
+
find(name) {
|
|
245
|
+
return this.commands.get(name) ?? this.commands.get(this.aliases.get(name) ?? "");
|
|
246
|
+
}
|
|
247
|
+
async ui() {
|
|
248
|
+
if (this.options.ui)
|
|
249
|
+
return this.options.ui;
|
|
250
|
+
const { createUi } = await import("./console-ui.js");
|
|
251
|
+
return createUi();
|
|
252
|
+
}
|
|
253
|
+
async prompt() {
|
|
254
|
+
if (this.options.prompt)
|
|
255
|
+
return this.options.prompt;
|
|
256
|
+
const { createPrompt } = await import("./console-prompt.js");
|
|
257
|
+
return createPrompt();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Run a command from argv (without the node/script prefix). Returns the exit
|
|
261
|
+
* code: 0 for success, 1 for a failure — never throws, because a console is a
|
|
262
|
+
* bad place to surface a stack trace to a user who mistyped a flag.
|
|
263
|
+
*/
|
|
264
|
+
async run(argv) {
|
|
265
|
+
const ui = await this.ui();
|
|
266
|
+
const [name, ...rest] = argv;
|
|
267
|
+
// No command, or an explicit ask for help: list what's available.
|
|
268
|
+
if (!name || name === "help" || name === "--help" || name === "-h") {
|
|
269
|
+
const target = name === "help" ? rest[0] : undefined;
|
|
270
|
+
if (target) {
|
|
271
|
+
const command = this.find(target);
|
|
272
|
+
if (!command) {
|
|
273
|
+
ui.error(`Unknown command "${target}".`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
ui.write(this.commandHelp(command));
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
ui.write(this.help());
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
const command = this.find(name);
|
|
283
|
+
if (!command) {
|
|
284
|
+
ui.error(`Unknown command "${name}". Run "${this.binary} help" to see what's available.`);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
// `--help` on a command shows its own help rather than running it.
|
|
288
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
289
|
+
ui.write(this.commandHelp(command));
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const parsed = parseArgv(rest, command);
|
|
294
|
+
const prompt = await this.prompt();
|
|
295
|
+
const code = await command.run({
|
|
296
|
+
args: parsed.args,
|
|
297
|
+
flags: parsed.flags,
|
|
298
|
+
rest: parsed.rest,
|
|
299
|
+
ui,
|
|
300
|
+
prompt,
|
|
301
|
+
kernel: this,
|
|
302
|
+
});
|
|
303
|
+
return typeof code === "number" ? code : 0;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
// A usage error is the user's mistake — say what's wrong and how to fix it,
|
|
307
|
+
// don't dump a stack at them.
|
|
308
|
+
if (error instanceof ConsoleError) {
|
|
309
|
+
ui.error(error.message);
|
|
310
|
+
ui.write("");
|
|
311
|
+
ui.write(this.commandHelp(command));
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
ui.error(error instanceof Error ? error.message : String(error));
|
|
315
|
+
if (error instanceof Error && error.stack && process.env.KEEL_DEBUG) {
|
|
316
|
+
ui.write(error.stack);
|
|
317
|
+
}
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** The `keel help` screen: every command, grouped by namespace. */
|
|
322
|
+
help() {
|
|
323
|
+
const lines = [`Usage: ${this.binary} <command> [options]`, ""];
|
|
324
|
+
const groups = new Map();
|
|
325
|
+
for (const command of this.list()) {
|
|
326
|
+
const namespace = command.name.includes(":") ? command.name.split(":")[0] : "";
|
|
327
|
+
const group = groups.get(namespace) ?? [];
|
|
328
|
+
group.push(command);
|
|
329
|
+
groups.set(namespace, group);
|
|
330
|
+
}
|
|
331
|
+
// Ungrouped commands first, then each namespace.
|
|
332
|
+
const order = [...groups.keys()].sort((a, b) => (a === "" ? -1 : b === "" ? 1 : a.localeCompare(b)));
|
|
333
|
+
const width = Math.max(...this.list().map((c) => c.name.length), 0);
|
|
334
|
+
for (const namespace of order) {
|
|
335
|
+
lines.push(namespace ? `${namespace}:` : "Commands:");
|
|
336
|
+
for (const command of groups.get(namespace)) {
|
|
337
|
+
lines.push(` ${command.name.padEnd(width + 2)}${command.description ?? ""}`);
|
|
338
|
+
}
|
|
339
|
+
lines.push("");
|
|
340
|
+
}
|
|
341
|
+
lines.push(`Run "${this.binary} help <command>" for more on one of them.`);
|
|
342
|
+
return lines.join("\n");
|
|
343
|
+
}
|
|
344
|
+
/** The help screen for a single command: its usage, args, and flags. */
|
|
345
|
+
commandHelp(command) {
|
|
346
|
+
const args = Object.entries(command.args ?? {});
|
|
347
|
+
const flags = Object.entries(command.flags ?? {});
|
|
348
|
+
const usage = [`${this.binary} ${command.name}`];
|
|
349
|
+
for (const [name, spec] of args) {
|
|
350
|
+
const label = spec.type === "spread" ? `...${name}` : name;
|
|
351
|
+
usage.push(spec.required && spec.default === undefined ? `<${label}>` : `[${label}]`);
|
|
352
|
+
}
|
|
353
|
+
if (flags.length)
|
|
354
|
+
usage.push("[options]");
|
|
355
|
+
const lines = [];
|
|
356
|
+
if (command.description)
|
|
357
|
+
lines.push(command.description, "");
|
|
358
|
+
lines.push(`Usage: ${usage.join(" ")}`, "");
|
|
359
|
+
if (args.length) {
|
|
360
|
+
lines.push("Arguments:");
|
|
361
|
+
const width = Math.max(...args.map(([n]) => n.length));
|
|
362
|
+
for (const [name, spec] of args) {
|
|
363
|
+
const meta = [];
|
|
364
|
+
if (!spec.required || spec.default !== undefined)
|
|
365
|
+
meta.push("optional");
|
|
366
|
+
if (spec.default !== undefined)
|
|
367
|
+
meta.push(`default: ${JSON.stringify(spec.default)}`);
|
|
368
|
+
const suffix = meta.length ? ` (${meta.join(", ")})` : "";
|
|
369
|
+
lines.push(` ${name.padEnd(width + 2)}${spec.description ?? ""}${suffix}`);
|
|
370
|
+
}
|
|
371
|
+
lines.push("");
|
|
372
|
+
}
|
|
373
|
+
if (flags.length) {
|
|
374
|
+
lines.push("Options:");
|
|
375
|
+
const labels = flags.map(([name, spec]) => (spec.alias ? `-${spec.alias}, --${name}` : ` --${name}`));
|
|
376
|
+
const width = Math.max(...labels.map((l) => l.length));
|
|
377
|
+
flags.forEach(([, spec], i) => {
|
|
378
|
+
const meta = [];
|
|
379
|
+
if (spec.required)
|
|
380
|
+
meta.push("required");
|
|
381
|
+
if (spec.default !== undefined && spec.type !== "boolean" && spec.type !== "array") {
|
|
382
|
+
meta.push(`default: ${JSON.stringify(spec.default)}`);
|
|
383
|
+
}
|
|
384
|
+
const suffix = meta.length ? ` (${meta.join(", ")})` : "";
|
|
385
|
+
lines.push(` ${labels[i].padEnd(width + 2)}${spec.description ?? ""}${suffix}`);
|
|
386
|
+
});
|
|
387
|
+
lines.push("");
|
|
388
|
+
}
|
|
389
|
+
if (command.aliases?.length)
|
|
390
|
+
lines.push(`Aliases: ${command.aliases.join(", ")}`, "");
|
|
391
|
+
if (command.help?.length)
|
|
392
|
+
lines.push(...command.help, "");
|
|
393
|
+
return lines.join("\n").replace(/\n+$/, "");
|
|
394
|
+
}
|
|
395
|
+
}
|
package/dist/core/database.d.ts
CHANGED
|
@@ -28,11 +28,32 @@ export interface Connection {
|
|
|
28
28
|
select(sql: string, bindings: unknown[]): Promise<Row[]>;
|
|
29
29
|
/** Run an INSERT/UPDATE/DELETE and return write metadata. */
|
|
30
30
|
write(sql: string, bindings: unknown[]): Promise<WriteResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Start a transaction on a **dedicated** connection.
|
|
33
|
+
*
|
|
34
|
+
* This is optional, but it is not optional for a *pooled* driver, and getting
|
|
35
|
+
* that wrong is the classic way to ship a transaction that silently isn't one:
|
|
36
|
+
* a pool hands each statement to whichever connection is free, so a `BEGIN`
|
|
37
|
+
* and the `INSERT` after it can land on different connections. The `BEGIN`
|
|
38
|
+
* then wraps nothing, the `COMMIT` commits nothing, and a failure half-writes.
|
|
39
|
+
*
|
|
40
|
+
* A driver that pools MUST implement this by checking out one connection and
|
|
41
|
+
* running the whole transaction on it. A driver that owns a single connection
|
|
42
|
+
* (SQLite, libSQL, a bare `pg.Client`) can leave it out, and Keel falls back to
|
|
43
|
+
* issuing `BEGIN` / `COMMIT` / `ROLLBACK` on the connection it has.
|
|
44
|
+
*/
|
|
45
|
+
begin?(): Promise<TransactionConnection>;
|
|
46
|
+
}
|
|
47
|
+
/** A connection with an open transaction on it. */
|
|
48
|
+
export interface TransactionConnection extends Connection {
|
|
49
|
+
commit(): Promise<void>;
|
|
50
|
+
rollback(): Promise<void>;
|
|
31
51
|
}
|
|
32
52
|
export type Dialect = "sqlite" | "mysql" | "postgres";
|
|
33
53
|
export type Operator = "=" | "!=" | "<" | "<=" | ">" | ">=" | "like";
|
|
34
|
-
/** A registered connection — the driver bridge
|
|
54
|
+
/** A registered connection — the driver bridge, its name, and its SQL dialect. */
|
|
35
55
|
interface Source {
|
|
56
|
+
name: string;
|
|
36
57
|
conn: Connection;
|
|
37
58
|
dialect: Dialect;
|
|
38
59
|
}
|
|
@@ -124,5 +145,53 @@ export interface ConnectionHandle {
|
|
|
124
145
|
* const reporting = connection("reporting");
|
|
125
146
|
* await reporting.table("events").where("kind", "signup").count();
|
|
126
147
|
*/
|
|
148
|
+
/** A handle to the transaction in flight, handed to `transaction()`'s callback. */
|
|
149
|
+
export interface TransactionHandle extends ConnectionHandle {
|
|
150
|
+
/**
|
|
151
|
+
* Roll back now and abandon the rest of the transaction. The callback should
|
|
152
|
+
* return straight after — queries on a rolled-back transaction will fail.
|
|
153
|
+
*/
|
|
154
|
+
rollback(): Promise<void>;
|
|
155
|
+
/** 0 for the outermost transaction; deeper values are savepoints. */
|
|
156
|
+
readonly depth: number;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Run `fn` inside a database transaction. It commits when `fn` returns, and
|
|
160
|
+
* **rolls back if `fn` throws** — which is the entire point: two related writes
|
|
161
|
+
* either both land or neither does, so a failure between them can't leave the row
|
|
162
|
+
* charged and the order missing.
|
|
163
|
+
*
|
|
164
|
+
* await transaction(async () => {
|
|
165
|
+
* await db("orders").insert(order);
|
|
166
|
+
* await db("stock").where("id", id).decrement("count"); // a throw here undoes the insert
|
|
167
|
+
* });
|
|
168
|
+
*
|
|
169
|
+
* Queries inside are **ambient**: `db()`, models, and relations all pick up the
|
|
170
|
+
* transaction's connection without being handed it. The callback also gets an
|
|
171
|
+
* explicit handle if you'd rather be obvious about it:
|
|
172
|
+
*
|
|
173
|
+
* await transaction(async (tx) => {
|
|
174
|
+
* await tx.table("orders").insert(order);
|
|
175
|
+
* });
|
|
176
|
+
*
|
|
177
|
+
* **Nesting uses savepoints.** A `transaction()` inside another doesn't open a
|
|
178
|
+
* second transaction — databases don't have those — it takes a savepoint, so an
|
|
179
|
+
* inner failure rolls back only the inner work and the outer transaction carries
|
|
180
|
+
* on. Without that, a nested helper's failure would silently abandon its caller's
|
|
181
|
+
* writes too.
|
|
182
|
+
*/
|
|
183
|
+
export declare function transaction<T>(fn: (tx: TransactionHandle) => Promise<T> | T, connectionName?: string): Promise<T>;
|
|
184
|
+
/** Whether a transaction is open on this connection in the current context. */
|
|
185
|
+
export declare function inTransaction(connectionName?: string): boolean;
|
|
127
186
|
export declare function connection(name?: string): ConnectionHandle;
|
|
187
|
+
/**
|
|
188
|
+
* The raw driver bridge and dialect for a connection (or the default) — what the
|
|
189
|
+
* `Migrator` needs. Unlike `connection()`, this hands back the unadjusted
|
|
190
|
+
* `Connection` so the migrator can apply its own placeholder conversion.
|
|
191
|
+
*/
|
|
192
|
+
export declare function getConnection(name?: string): {
|
|
193
|
+
connection: Connection;
|
|
194
|
+
dialect: Dialect;
|
|
195
|
+
name: string;
|
|
196
|
+
};
|
|
128
197
|
export {};
|