@agentic-kit/db-tools 0.11.4 → 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.
- package/README.md +40 -1
- package/app-workspace.d.ts +29 -2
- package/app-workspace.js +99 -8
- package/blueprint-construction.d.ts +60 -0
- package/blueprint-construction.js +161 -0
- package/context.d.ts +5 -1
- package/context.js +30 -5
- package/db-probe.d.ts +25 -17
- package/db-probe.js +97 -37
- package/esm/app-workspace.d.ts +29 -2
- package/esm/app-workspace.js +96 -8
- package/esm/blueprint-construction.d.ts +60 -0
- package/esm/blueprint-construction.js +154 -0
- package/esm/context.d.ts +5 -1
- package/esm/context.js +31 -6
- package/esm/db-probe.d.ts +25 -17
- package/esm/db-probe.js +96 -37
- package/esm/graphql-http.d.ts +35 -0
- package/esm/graphql-http.js +134 -0
- package/esm/host.d.ts +46 -0
- package/esm/host.js +9 -0
- package/esm/index.d.ts +11 -2
- package/esm/index.js +6 -0
- package/esm/provision-database/env-file.d.ts +23 -2
- package/esm/provision-database/env-file.js +86 -27
- package/esm/provision-database/pending-ticket.d.ts +33 -0
- package/esm/provision-database/pending-ticket.js +63 -0
- package/esm/provision-database/provision.d.ts +71 -0
- package/esm/provision-database/provision.js +557 -0
- package/esm/provision-database/request-database.d.ts +55 -3
- package/esm/provision-database/request-database.js +118 -17
- package/esm/schema-model/normalize.d.ts +21 -0
- package/esm/schema-model/normalize.js +244 -0
- package/esm/schema-model.d.ts +110 -0
- package/esm/schema-model.js +303 -0
- package/esm/tools/add-policies.d.ts +1 -1
- package/esm/tools/add-records.d.ts +1 -0
- package/esm/tools/add-records.js +18 -6
- package/esm/tools/add-relation.js +2 -3
- package/esm/tools/create-api-key.d.ts +2 -0
- package/esm/tools/create-api-key.js +27 -12
- package/esm/tools/manage-entity-types.d.ts +1 -1
- package/esm/tools/provision-blueprint.d.ts +6 -0
- package/esm/tools/provision-blueprint.js +11 -4
- package/esm/tools/provision-database.d.ts +8 -11
- package/esm/tools/provision-database.js +23 -243
- package/esm/tools/run-codegen.d.ts +19 -2
- package/esm/tools/run-codegen.js +184 -97
- package/esm/tools/templates.js +3 -15
- package/graphql-http.d.ts +35 -0
- package/graphql-http.js +140 -0
- package/host.d.ts +46 -0
- package/host.js +10 -0
- package/index.d.ts +11 -2
- package/index.js +26 -1
- package/package.json +4 -4
- package/provision-database/env-file.d.ts +23 -2
- package/provision-database/env-file.js +89 -28
- package/provision-database/pending-ticket.d.ts +33 -0
- package/provision-database/pending-ticket.js +72 -0
- package/provision-database/provision.d.ts +71 -0
- package/provision-database/provision.js +565 -0
- package/provision-database/request-database.d.ts +55 -3
- package/provision-database/request-database.js +124 -18
- package/schema-model/normalize.d.ts +21 -0
- package/schema-model/normalize.js +250 -0
- package/schema-model.d.ts +110 -0
- package/schema-model.js +309 -0
- package/tools/add-policies.d.ts +1 -1
- package/tools/add-records.d.ts +1 -0
- package/tools/add-records.js +18 -6
- package/tools/add-relation.js +2 -3
- package/tools/create-api-key.d.ts +2 -0
- package/tools/create-api-key.js +27 -12
- package/tools/manage-entity-types.d.ts +1 -1
- package/tools/provision-blueprint.d.ts +6 -0
- package/tools/provision-blueprint.js +11 -4
- package/tools/provision-database.d.ts +8 -11
- package/tools/provision-database.js +23 -245
- package/tools/run-codegen.d.ts +19 -2
- package/tools/run-codegen.js +184 -96
- package/tools/templates.js +3 -15
|
@@ -1,26 +1,92 @@
|
|
|
1
|
-
// Pure .env
|
|
2
|
-
// credentials without clobbering existing keys
|
|
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`.
|
|
7
|
-
* in place (
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
75
|
-
.map((line) => {
|
|
76
|
-
|
|
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 {};
|