@agentic-kit/db-tools 0.11.5 → 0.12.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.
Files changed (82) hide show
  1. package/README.md +40 -1
  2. package/app-workspace.d.ts +29 -2
  3. package/app-workspace.js +99 -8
  4. package/blueprint-construction.d.ts +60 -0
  5. package/blueprint-construction.js +161 -0
  6. package/context.d.ts +5 -1
  7. package/context.js +30 -5
  8. package/db-probe.d.ts +25 -17
  9. package/db-probe.js +97 -37
  10. package/esm/app-workspace.d.ts +29 -2
  11. package/esm/app-workspace.js +96 -8
  12. package/esm/blueprint-construction.d.ts +60 -0
  13. package/esm/blueprint-construction.js +154 -0
  14. package/esm/context.d.ts +5 -1
  15. package/esm/context.js +31 -6
  16. package/esm/db-probe.d.ts +25 -17
  17. package/esm/db-probe.js +96 -37
  18. package/esm/graphql-http.d.ts +35 -0
  19. package/esm/graphql-http.js +134 -0
  20. package/esm/host.d.ts +46 -0
  21. package/esm/host.js +9 -0
  22. package/esm/index.d.ts +11 -2
  23. package/esm/index.js +6 -0
  24. package/esm/provision-database/env-file.d.ts +23 -2
  25. package/esm/provision-database/env-file.js +86 -27
  26. package/esm/provision-database/pending-ticket.d.ts +33 -0
  27. package/esm/provision-database/pending-ticket.js +63 -0
  28. package/esm/provision-database/provision.d.ts +71 -0
  29. package/esm/provision-database/provision.js +557 -0
  30. package/esm/provision-database/request-database.d.ts +55 -3
  31. package/esm/provision-database/request-database.js +118 -17
  32. package/esm/schema-model/normalize.d.ts +21 -0
  33. package/esm/schema-model/normalize.js +244 -0
  34. package/esm/schema-model.d.ts +110 -0
  35. package/esm/schema-model.js +303 -0
  36. package/esm/tools/add-policies.d.ts +1 -1
  37. package/esm/tools/add-records.d.ts +1 -0
  38. package/esm/tools/add-records.js +18 -6
  39. package/esm/tools/add-relation.js +2 -3
  40. package/esm/tools/create-api-key.d.ts +2 -0
  41. package/esm/tools/create-api-key.js +27 -12
  42. package/esm/tools/manage-entity-types.d.ts +1 -1
  43. package/esm/tools/provision-blueprint.d.ts +6 -0
  44. package/esm/tools/provision-blueprint.js +11 -4
  45. package/esm/tools/provision-database.d.ts +8 -11
  46. package/esm/tools/provision-database.js +23 -243
  47. package/esm/tools/run-codegen.d.ts +19 -2
  48. package/esm/tools/run-codegen.js +184 -97
  49. package/esm/tools/templates.js +3 -15
  50. package/graphql-http.d.ts +35 -0
  51. package/graphql-http.js +140 -0
  52. package/host.d.ts +46 -0
  53. package/host.js +10 -0
  54. package/index.d.ts +11 -2
  55. package/index.js +26 -1
  56. package/package.json +4 -4
  57. package/provision-database/env-file.d.ts +23 -2
  58. package/provision-database/env-file.js +89 -28
  59. package/provision-database/pending-ticket.d.ts +33 -0
  60. package/provision-database/pending-ticket.js +72 -0
  61. package/provision-database/provision.d.ts +71 -0
  62. package/provision-database/provision.js +565 -0
  63. package/provision-database/request-database.d.ts +55 -3
  64. package/provision-database/request-database.js +124 -18
  65. package/schema-model/normalize.d.ts +21 -0
  66. package/schema-model/normalize.js +250 -0
  67. package/schema-model.d.ts +110 -0
  68. package/schema-model.js +309 -0
  69. package/tools/add-policies.d.ts +1 -1
  70. package/tools/add-records.d.ts +1 -0
  71. package/tools/add-records.js +18 -6
  72. package/tools/add-relation.js +2 -3
  73. package/tools/create-api-key.d.ts +2 -0
  74. package/tools/create-api-key.js +27 -12
  75. package/tools/manage-entity-types.d.ts +1 -1
  76. package/tools/provision-blueprint.d.ts +6 -0
  77. package/tools/provision-blueprint.js +11 -4
  78. package/tools/provision-database.d.ts +8 -11
  79. package/tools/provision-database.js +23 -245
  80. package/tools/run-codegen.d.ts +19 -2
  81. package/tools/run-codegen.js +184 -96
  82. package/tools/templates.js +3 -15
@@ -1,26 +1,92 @@
1
- // Pure .env merge — used by provision_database to persist provisioning
2
- // credentials without clobbering existing keys: replace a key in place if
3
- // present, else append.
1
+ // Pure .env edits — used by provision_database to persist provisioning
2
+ // credentials without clobbering existing keys.
4
3
  // Kept side-effect-free so it is unit-testable (the tool does the fs write).
4
+ //
5
+ // Reads go through the same parser context.ts uses (quotes, `export`,
6
+ // comments, the last definition wins), and edits find a key the way that
7
+ // parser does, so a key written as `export KEY="value"` or spread over several
8
+ // lines by a quoted value is still the key being replaced or removed.
9
+ import { parseDotenv } from '12factor-env/dotenv';
10
+ /** Parse .env text: quoted values, `export` prefixes and comments; last definition wins. */
11
+ export function readEnv(content) {
12
+ return parseDotenv(content);
13
+ }
14
+ const ENTRY_RE = /^(\s*)(export\s+)?([\w.-]+)\s*=\s*(.*)$/;
15
+ const QUOTES = new Set(['"', "'", '`']);
16
+ // Group lines into entries: a key with the lines its value spans (a quoted
17
+ // value left open continues to the line that closes it), or a non-entry line
18
+ // (comment, blank, anything else) with a null key.
19
+ function envEntries(content) {
20
+ const lines = content.split('\n');
21
+ const entries = [];
22
+ for (let i = 0; i < lines.length; i++) {
23
+ const line = lines[i];
24
+ const match = line.trimStart().startsWith('#') ? null : ENTRY_RE.exec(line);
25
+ if (!match) {
26
+ entries.push({ key: null, exported: false, lines: [line] });
27
+ continue;
28
+ }
29
+ const value = match[4];
30
+ const span = [line];
31
+ if (QUOTES.has(value[0]) && !value.slice(1).includes(value[0])) {
32
+ while (i + 1 < lines.length) {
33
+ span.push(lines[++i]);
34
+ if (lines[i].includes(value[0]))
35
+ break;
36
+ }
37
+ }
38
+ entries.push({ key: match[3], exported: !!match[2], lines: span });
39
+ }
40
+ return entries;
41
+ }
5
42
  /**
6
- * Upsert `vars` into the existing `.env` `content`. Existing keys are replaced
7
- * in place (preserving surrounding lines/comments); new keys are appended.
43
+ * Upsert `vars` into the existing `.env` `content`. An existing key is
44
+ * replaced in place (keeping an `export` prefix), and any later definition of
45
+ * it is dropped so the new value is the one that wins; new keys are appended.
8
46
  * Returns the new file body, trailing-newline-terminated.
9
47
  */
10
48
  export function mergeEnv(content, vars) {
11
- let out = content;
12
- for (const [key, val] of Object.entries(vars)) {
13
- // Escape the key for the regex, and use a replacement function so `$`
14
- // sequences in the value are never treated as replacement specials.
15
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
- const regex = new RegExp(`^${escapedKey}=.*`, 'm');
17
- if (regex.test(out))
18
- out = out.replace(regex, () => `${key}=${val}`);
19
- else
20
- out += `\n${key}=${val}`;
49
+ const remaining = new Map(Object.entries(vars));
50
+ const written = new Set();
51
+ const out = [];
52
+ for (const entry of envEntries(content)) {
53
+ if (entry.key !== null && remaining.has(entry.key)) {
54
+ out.push(`${entry.exported ? 'export ' : ''}${entry.key}=${remaining.get(entry.key)}`);
55
+ remaining.delete(entry.key);
56
+ written.add(entry.key);
57
+ }
58
+ else if (entry.key === null || !written.has(entry.key)) {
59
+ out.push(...entry.lines);
60
+ }
21
61
  }
22
- return out.trim() + '\n';
62
+ let text = out.join('\n');
63
+ for (const [key, value] of remaining)
64
+ text += `\n${key}=${value}`;
65
+ return text.trim() + '\n';
23
66
  }
67
+ /**
68
+ * Drop every definition of `keys`. Comments and other entries pass through,
69
+ * and content without those keys comes back unchanged.
70
+ */
71
+ export function removeEnvKeys(content, keys) {
72
+ const keySet = new Set(keys);
73
+ const entries = envEntries(content);
74
+ const kept = entries.filter((entry) => entry.key === null || !keySet.has(entry.key));
75
+ return kept.length === entries.length ? content : kept.flatMap((entry) => entry.lines).join('\n');
76
+ }
77
+ /**
78
+ * The provision request provision_database is still waiting on, recorded when
79
+ * it stops waiting so the next call resumes that ticket. It's resumed only
80
+ * for the same database name, backend and account. Absent whenever no request
81
+ * is pending.
82
+ */
83
+ export const PENDING_PROVISION_KEYS = {
84
+ ticketId: 'PROVISION_TICKET_ID',
85
+ databaseName: 'PROVISION_TICKET_DATABASE_NAME',
86
+ apiEndpoint: 'PROVISION_TICKET_API_ENDPOINT',
87
+ userId: 'PROVISION_TICKET_USER_ID',
88
+ attempts: 'PROVISION_TICKET_ATTEMPTS',
89
+ };
24
90
  /** The credential keys provision_database owns in a project's .env. */
25
91
  export const PROVISION_ENV_KEYS = [
26
92
  'DATABASE_ID',
@@ -70,16 +136,9 @@ export function provisionEnvVars(args) {
70
136
  */
71
137
  export function archiveBindingKeys(content, keys, archivedOn) {
72
138
  const keySet = new Set(keys);
73
- return content
74
- .split('\n')
75
- .map((line) => {
76
- if (line.trimStart().startsWith('#'))
77
- return line;
78
- const eq = line.indexOf('=');
79
- if (eq === -1)
80
- return line;
81
- const key = line.slice(0, eq).trim();
82
- return keySet.has(key) ? `# archived ${archivedOn}: ${line.trim()}` : line;
83
- })
139
+ return envEntries(content)
140
+ .flatMap((entry) => entry.key !== null && keySet.has(entry.key)
141
+ ? entry.lines.map((line) => `# archived ${archivedOn}: ${line.trim()}`)
142
+ : entry.lines)
84
143
  .join('\n');
85
144
  }
@@ -0,0 +1,33 @@
1
+ import { type EnvVars } from './env-file';
2
+ /**
3
+ * Pending results recorded for one ticket before provision_database stops
4
+ * following it. At the default 240 s deadline that is about 20 minutes.
5
+ */
6
+ export declare const MAX_PENDING_ATTEMPTS = 5;
7
+ export type PendingTicket = {
8
+ ticketId: string;
9
+ databaseName: string;
10
+ apiEndpoint: string;
11
+ userId: string;
12
+ /** Pending results recorded for this ticket so far. */
13
+ attempts: number;
14
+ };
15
+ /**
16
+ * Tickets the host told provisioning to stop following (`abandonPending`),
17
+ * comma-separated. A lookup by name skips them, so an abandoned provision is
18
+ * never resumed.
19
+ */
20
+ export declare const ABANDONED_TICKETS_KEY = "PROVISION_TICKET_ABANDONED";
21
+ /**
22
+ * The recorded ticket, with the name, backend and account it was requested
23
+ * for. Only a ticket from this backend and account is ours to poll, and one
24
+ * for another name is a different database.
25
+ */
26
+ export declare function recordedTicket(env: EnvVars): PendingTicket | undefined;
27
+ export declare function recordPendingTicket(content: string, ticket: PendingTicket): string;
28
+ export declare function clearPendingTicket(content: string): string;
29
+ /** Comment the record out (`# archived <date>: …`), keeping it readable. */
30
+ export declare function archivePendingTicket(content: string, archivedOn: string): string;
31
+ export declare function abandonedTickets(env: EnvVars): Set<string>;
32
+ /** Add ticket ids to the abandoned list. */
33
+ export declare function recordAbandoned(content: string, ticketIds: readonly string[]): string;
@@ -0,0 +1,63 @@
1
+ // The provision request provision_database stopped waiting on, as recorded in
2
+ // a project's .env. requestDatabase never resumes by name, so the ticket is
3
+ // the only way back to a provision that is still running.
4
+ import { archiveBindingKeys, mergeEnv, PENDING_PROVISION_KEYS, readEnv, removeEnvKeys, } from './env-file';
5
+ /**
6
+ * Pending results recorded for one ticket before provision_database stops
7
+ * following it. At the default 240 s deadline that is about 20 minutes.
8
+ */
9
+ export const MAX_PENDING_ATTEMPTS = 5;
10
+ const KEYS = Object.values(PENDING_PROVISION_KEYS);
11
+ /**
12
+ * Tickets the host told provisioning to stop following (`abandonPending`),
13
+ * comma-separated. A lookup by name skips them, so an abandoned provision is
14
+ * never resumed.
15
+ */
16
+ export const ABANDONED_TICKETS_KEY = 'PROVISION_TICKET_ABANDONED';
17
+ /** The newest abandoned ids kept; older ones are long finished or failed. */
18
+ const MAX_ABANDONED = 10;
19
+ /**
20
+ * The recorded ticket, with the name, backend and account it was requested
21
+ * for. Only a ticket from this backend and account is ours to poll, and one
22
+ * for another name is a different database.
23
+ */
24
+ export function recordedTicket(env) {
25
+ const ticketId = env[PENDING_PROVISION_KEYS.ticketId];
26
+ if (!ticketId)
27
+ return undefined;
28
+ const attempts = Number.parseInt(env[PENDING_PROVISION_KEYS.attempts] ?? '', 10);
29
+ return {
30
+ ticketId,
31
+ databaseName: env[PENDING_PROVISION_KEYS.databaseName] ?? '',
32
+ apiEndpoint: env[PENDING_PROVISION_KEYS.apiEndpoint] ?? '',
33
+ userId: env[PENDING_PROVISION_KEYS.userId] ?? '',
34
+ attempts: Number.isFinite(attempts) && attempts > 0 ? attempts : 0,
35
+ };
36
+ }
37
+ export function recordPendingTicket(content, ticket) {
38
+ return mergeEnv(content, {
39
+ [PENDING_PROVISION_KEYS.ticketId]: ticket.ticketId,
40
+ [PENDING_PROVISION_KEYS.databaseName]: ticket.databaseName,
41
+ [PENDING_PROVISION_KEYS.apiEndpoint]: ticket.apiEndpoint,
42
+ [PENDING_PROVISION_KEYS.userId]: ticket.userId,
43
+ [PENDING_PROVISION_KEYS.attempts]: String(ticket.attempts),
44
+ });
45
+ }
46
+ export function clearPendingTicket(content) {
47
+ return removeEnvKeys(content, KEYS);
48
+ }
49
+ /** Comment the record out (`# archived <date>: …`), keeping it readable. */
50
+ export function archivePendingTicket(content, archivedOn) {
51
+ return archiveBindingKeys(content, KEYS, archivedOn);
52
+ }
53
+ export function abandonedTickets(env) {
54
+ return new Set((env[ABANDONED_TICKETS_KEY] ?? '')
55
+ .split(',')
56
+ .map((id) => id.trim())
57
+ .filter(Boolean));
58
+ }
59
+ /** Add ticket ids to the abandoned list. */
60
+ export function recordAbandoned(content, ticketIds) {
61
+ const ids = [...new Set([...abandonedTickets(readEnv(content)), ...ticketIds])].slice(-MAX_ABANDONED);
62
+ return mergeEnv(content, { [ABANDONED_TICKETS_KEY]: ids.join(',') });
63
+ }
@@ -0,0 +1,71 @@
1
+ import { type RunCommand } from '../app-workspace';
2
+ export type ProvisionInput = {
3
+ databaseName: string;
4
+ /** `ensure` keeps a live binding; `replace` swaps the binding for a new database. */
5
+ mode?: 'ensure' | 'replace';
6
+ /**
7
+ * Replace mode: the DATABASE_ID the caller expects bound (null: none). Any
8
+ * other binding fails `binding-changed`. Undefined replaces whatever is bound.
9
+ */
10
+ replaces?: string | null;
11
+ /** Host only: stop following an in-flight provision and request a new one. */
12
+ abandonPending?: boolean;
13
+ /**
14
+ * Host only: bind this database, which the signed-in account owns and is
15
+ * active, without requesting one (recovery after `name-taken`).
16
+ */
17
+ adopt?: string;
18
+ };
19
+ export type ProvisionFailureCode = 'signed-out' | 'backend-unreachable' | 'db-missing' | 'db-suspended' | 'account-mismatch' | 'binding-live' | 'binding-changed' | 'pending-other' | 'ticket-lost' | 'provision-stuck' | 'name-taken' | 'binding-not-saved' | 'env-write-failed' | 'reprovision-not-allowed' | 'failed';
20
+ export type ProvisionDatabaseDetails = {
21
+ success: boolean;
22
+ message: string;
23
+ /** Why it failed; absent on success and on a pending result. */
24
+ code?: ProvisionFailureCode;
25
+ databaseId?: string;
26
+ databaseName?: string;
27
+ ownerId?: string;
28
+ /** True when an existing live binding was kept: nothing changed. */
29
+ skipped?: boolean;
30
+ /**
31
+ * True when the call stopped waiting while the provision is still running.
32
+ * Calling again with the same database_name resumes it.
33
+ */
34
+ pending?: boolean;
35
+ /** The provision ticket, on a pending or stuck result. */
36
+ ticketId?: string;
37
+ /** True when this call finished a provision an earlier call had started. */
38
+ resumed?: boolean;
39
+ /** True when that provision was found on the backend by name, not in `.env`. */
40
+ recovered?: boolean;
41
+ /** The database the new binding replaced (replace mode). */
42
+ replaced?: string;
43
+ /** With `name-taken`: a database of this account holds the name. Never bound automatically. */
44
+ heldBy?: {
45
+ databaseId: string;
46
+ yours: boolean;
47
+ createdAt?: string;
48
+ };
49
+ };
50
+ type Details = ProvisionDatabaseDetails;
51
+ /** The DATABASE_ID in a project's `.env`, if any. */
52
+ export declare function boundDatabaseId(cwd: string): Promise<string | undefined>;
53
+ /**
54
+ * Provision the project's database, or keep the one it is bound to. The host
55
+ * entry point: replace mode refuses a live binding here, and `abandonPending`
56
+ * and `adopt` are honoured.
57
+ */
58
+ export declare function provisionDatabase(input: ProvisionInput, ctx: {
59
+ cwd: string;
60
+ signal?: AbortSignal;
61
+ }, exec?: RunCommand): Promise<Details>;
62
+ /**
63
+ * The provision_database tool's entry point: its `reprovision: true` may
64
+ * replace a live binding (a clean rebuild), unless the host turned tool
65
+ * reprovisioning off.
66
+ */
67
+ export declare function provisionFromTool(input: Pick<ProvisionInput, 'databaseName' | 'mode' | 'replaces'>, ctx: {
68
+ cwd: string;
69
+ signal?: AbortSignal;
70
+ }, exec?: RunCommand): Promise<Details>;
71
+ export {};