@agentic-kit/db-tools 0.11.5 → 0.12.1

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
package/esm/db-probe.d.ts CHANGED
@@ -1,30 +1,38 @@
1
+ import { postGraphQL } from './graphql-http';
2
+ export type UnreachableCause = 'network' | 'server' | 'auth' | 'error';
1
3
  export type DatabaseProbe = {
2
- outcome: 'found';
4
+ status: 'found';
5
+ databaseId: string;
6
+ name?: string;
7
+ ownerId?: string;
8
+ hash?: string;
9
+ } | {
10
+ status: 'suspended';
11
+ databaseId: string;
3
12
  name?: string;
4
13
  ownerId?: string;
14
+ hash?: string;
15
+ suspendedAt: string;
16
+ suspendedReason?: string;
5
17
  } | {
6
- outcome: 'missing';
18
+ status: 'missing';
19
+ databaseId: string;
7
20
  } | {
8
- outcome: 'unreachable';
21
+ status: 'unreachable';
22
+ databaseId: string;
23
+ cause: UnreachableCause;
9
24
  detail: string;
10
25
  };
11
- export type ProbeExecutor = {
12
- execute<T>(document: string, variables?: Record<string, unknown>): Promise<{
13
- ok: true;
14
- data: T;
15
- errors: undefined;
16
- } | {
17
- ok: false;
18
- data: null;
19
- errors: {
20
- message: string;
21
- }[];
22
- }>;
23
- };
24
26
  export declare function probeDatabase(args: {
25
27
  endpoint: string;
26
28
  bearer: string;
27
29
  databaseId: string;
28
- executor?: ProbeExecutor;
29
30
  signInHint?: string;
31
+ post?: typeof postGraphQL;
30
32
  }): Promise<DatabaseProbe>;
33
+ /**
34
+ * Probe a database with the host's account bearer on its configured control
35
+ * endpoint (never a project-pinned one). Without a bearer the answer is
36
+ * unreachable with cause 'auth'.
37
+ */
38
+ export declare function probeBoundDatabase(databaseId: string): Promise<DatabaseProbe>;
package/esm/db-probe.js CHANGED
@@ -1,47 +1,106 @@
1
- import { api } from '@constructive-io/sdk';
2
- const AUTH_ERROR_RE = /unauthenticated|unauthorized|permission denied|jwt|http 401|http 403/i;
3
- // Liveness probe for a bound database. The split drives recovery: 'missing'
4
- // means the backend answered and the binding is dead (database deleted,
5
- // backend refreshed) — recovery is reprovision. 'unreachable' means no usable
6
- // answer (backend down, network error, rejected credential) — recovery is
7
- // retry/re-sign-in, never reprovision.
1
+ import { classifyBackendErrors, errorsText, postGraphQL } from './graphql-http';
2
+ import { getHost } from './host';
3
+ const DEFAULT_API_ENDPOINT = 'http://api.localhost:3000/graphql';
4
+ const FIELDS = 'id name ownerId hash suspendedAt suspendedReason';
5
+ // A backend from before `hash` and the suspension columns.
6
+ const BASIC_FIELDS = 'id name ownerId';
7
+ const NEWER_FIELD_RE = /\b(hash|suspended_?at|suspended_?reason)\b/i;
8
+ const document = (databaseId, fields) => `
9
+ query ProbeDatabase {
10
+ databases(where: { id: { equalTo: ${JSON.stringify(databaseId)} } }, first: 1) {
11
+ nodes { ${fields} }
12
+ }
13
+ }`;
14
+ // Liveness probe for a bound database, on the platform api. Only 'missing'
15
+ // means the binding is dead: the backend answered and the account can't see
16
+ // the database (deleted, or access to it lost). 'suspended' means it exists
17
+ // and is held, so its data must not be replaced. 'unreachable' is any answer
18
+ // that says nothing about the database (no response, a failing backend, a
19
+ // rejected account credential, an error this probe doesn't know); recovery is
20
+ // a retry or a sign-in, never a reprovision.
8
21
  //
9
22
  // The probe authenticates with the ACCOUNT bearer, not the project's .env key:
10
23
  // the platform api rejects per-database keys, and the singular `database(id:)`
11
24
  // field is gone from the current contract, so the lookup goes through the
12
- // `databases` collection filter via the SDK's raw FetchAdapter.
25
+ // `databases` collection filter.
13
26
  export async function probeDatabase(args) {
14
- const executor = args.executor ??
15
- new api.FetchAdapter(args.endpoint, { Authorization: `Bearer ${args.bearer}` });
16
- const document = `
17
- query ProbeDatabase {
18
- databases(where: { id: { equalTo: ${JSON.stringify(args.databaseId)} } }) {
19
- nodes { id name ownerId }
20
- }
21
- }`;
22
- try {
23
- const result = await executor.execute(document);
24
- if (result.ok) {
25
- const node = result.data.databases?.nodes?.[0];
26
- if (!node)
27
- return { outcome: 'missing' };
28
- return { outcome: 'found', name: node.name ?? undefined, ownerId: node.ownerId ?? undefined };
29
- }
30
- const detail = result.errors?.[0]?.message ?? 'unknown error';
31
- // A 5xx is the backend failing, not an answer about this database. An auth
32
- // rejection is a dead ACCOUNT credential — never evidence the binding is
33
- // dead, so it must not push toward reprovision.
34
- if (/^HTTP 5\d\d/i.test(detail))
35
- return { outcome: 'unreachable', detail };
36
- if (AUTH_ERROR_RE.test(detail)) {
27
+ const post = args.post ?? postGraphQL;
28
+ const { databaseId } = args;
29
+ const ask = (fields) => post({
30
+ endpoint: args.endpoint,
31
+ headers: { Authorization: `Bearer ${args.bearer}` },
32
+ query: document(databaseId, fields),
33
+ });
34
+ let result = await ask(FIELDS);
35
+ if (result.errors.length > 0 && result.errors.some((e) => NEWER_FIELD_RE.test(e?.message ?? ''))) {
36
+ result = await ask(BASIC_FIELDS);
37
+ }
38
+ if (result.errors.length > 0)
39
+ return unreachable(databaseId, result.errors, args.signInHint);
40
+ const nodes = result.data?.databases?.nodes;
41
+ if (!Array.isArray(nodes)) {
42
+ return { status: 'unreachable', databaseId, cause: 'error', detail: 'the backend answered without a databases list' };
43
+ }
44
+ const node = nodes[0];
45
+ if (!node)
46
+ return { status: 'missing', databaseId };
47
+ const found = {
48
+ databaseId,
49
+ name: node.name ?? undefined,
50
+ ownerId: node.ownerId ?? undefined,
51
+ hash: node.hash ?? undefined,
52
+ };
53
+ if (node.suspendedAt) {
54
+ return {
55
+ status: 'suspended',
56
+ ...found,
57
+ suspendedAt: node.suspendedAt,
58
+ suspendedReason: node.suspendedReason ?? undefined,
59
+ };
60
+ }
61
+ return { status: 'found', ...found };
62
+ }
63
+ function unreachable(databaseId, errors, signInHint) {
64
+ const detail = errorsText(errors);
65
+ switch (classifyBackendErrors(errors)) {
66
+ case 'network':
67
+ return { status: 'unreachable', databaseId, cause: 'network', detail };
68
+ case 'server':
69
+ return { status: 'unreachable', databaseId, cause: 'server', detail };
70
+ case 'auth':
71
+ case 'forbidden':
72
+ // A rejected ACCOUNT credential is never evidence the binding is dead.
37
73
  return {
38
- outcome: 'unreachable',
39
- detail: `${detail} — the account credential was rejected. ${args.signInHint ?? 'Sign in again, then retry.'}`,
74
+ status: 'unreachable',
75
+ databaseId,
76
+ cause: 'auth',
77
+ detail: `${detail}: the account credential was rejected. ${signInHint ?? 'Sign in again, then retry.'}`,
40
78
  };
41
- }
42
- return { outcome: 'missing' };
79
+ default:
80
+ return { status: 'unreachable', databaseId, cause: 'error', detail };
43
81
  }
44
- catch (err) {
45
- return { outcome: 'unreachable', detail: err instanceof Error ? err.message : String(err) };
82
+ }
83
+ /**
84
+ * Probe a database with the host's account bearer on its configured control
85
+ * endpoint (never a project-pinned one). Without a bearer the answer is
86
+ * unreachable with cause 'auth'.
87
+ */
88
+ export async function probeBoundDatabase(databaseId) {
89
+ const host = getHost();
90
+ const account = host.account();
91
+ const bearer = account?.apiKey ?? account?.accessToken;
92
+ if (!bearer) {
93
+ return {
94
+ status: 'unreachable',
95
+ databaseId,
96
+ cause: 'auth',
97
+ detail: `No usable account credential. ${host.signInHint ?? 'Sign in to the app, then retry.'}`,
98
+ };
46
99
  }
100
+ return probeDatabase({
101
+ endpoint: host.backendConfig()?.apiEndpoint || DEFAULT_API_ENDPOINT,
102
+ bearer,
103
+ databaseId,
104
+ signInHint: host.signInHint,
105
+ });
47
106
  }
@@ -0,0 +1,35 @@
1
+ import { type GraphQLAdapter, type GraphQLError } from '@constructive-io/graphql-query/runtime';
2
+ export type { GraphQLError };
3
+ export type HttpGraphQLResponse<T> = {
4
+ /** The response status, or null when no response arrived. */
5
+ httpStatus: number | null;
6
+ data: T | null;
7
+ /**
8
+ * Body errors, each with `extensions.httpStatus` added. A non-2xx without
9
+ * body errors becomes one `{ code: 'HTTP_ERROR', httpStatus }` error, and a
10
+ * fetch that threw becomes one `{ code: 'NETWORK' }` error.
11
+ */
12
+ errors: GraphQLError[];
13
+ };
14
+ export type PostGraphQLArgs = {
15
+ endpoint: string;
16
+ headers?: Record<string, string>;
17
+ query: string;
18
+ variables?: Record<string, unknown>;
19
+ fetchFn?: typeof fetch;
20
+ };
21
+ export declare function postGraphQL<T>(args: PostGraphQLArgs): Promise<HttpGraphQLResponse<T>>;
22
+ /**
23
+ * An ORM adapter over `postGraphQL`: the SDK clients take it in place of their
24
+ * FetchAdapter, and their failures then carry the status and code.
25
+ */
26
+ export declare function statusAdapter(endpoint: string, headers?: Record<string, string>, fetchFn?: typeof fetch): GraphQLAdapter;
27
+ export type BackendErrorKind = 'suspended' | 'auth' | 'forbidden' | 'server' | 'network' | 'graphql';
28
+ /**
29
+ * What a failed backend answer says, first match wins: the database is
30
+ * suspended or gone (`ACCESS_SUSPENDED`), no response, the backend failing
31
+ * (5xx, 429), a rejected credential (401), a refusal (403), or anything else.
32
+ */
33
+ export declare function classifyBackendErrors(errors: readonly GraphQLError[]): BackendErrorKind;
34
+ /** The messages of a failed answer, joined for a reason string. */
35
+ export declare function errorsText(errors: readonly GraphQLError[]): string;
@@ -0,0 +1,134 @@
1
+ // A GraphQL POST that keeps what the SDK's FetchAdapter throws away. That
2
+ // adapter turns any non-2xx into `HTTP 403: Forbidden` and drops the body, so
3
+ // a tenant API's `ACCESS_SUSPENDED` (a 403 with a JSON body, sent for a
4
+ // deleted or suspended database) reads the same as any other refusal. Here
5
+ // the body's errors keep their `extensions.code`, and every error carries the
6
+ // HTTP status it arrived with.
7
+ import { createFetch, } from '@constructive-io/graphql-query/runtime';
8
+ const errorText = (err) => (err instanceof Error ? err.message : String(err));
9
+ export async function postGraphQL(args) {
10
+ const fetchFn = (args.fetchFn ?? createFetch()).bind(globalThis);
11
+ let response;
12
+ try {
13
+ response = await fetchFn(args.endpoint, {
14
+ method: 'POST',
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ Accept: 'application/json',
18
+ ...args.headers,
19
+ },
20
+ body: JSON.stringify({ query: args.query, variables: args.variables ?? {} }),
21
+ });
22
+ }
23
+ catch (err) {
24
+ const cause = err?.cause?.code;
25
+ return {
26
+ httpStatus: null,
27
+ data: null,
28
+ errors: [
29
+ {
30
+ message: typeof cause === 'string' ? `${errorText(err)} (${cause})` : errorText(err),
31
+ extensions: { code: 'NETWORK' },
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ const httpStatus = response.status;
37
+ let body = null;
38
+ try {
39
+ body = (await response.json());
40
+ }
41
+ catch {
42
+ body = null;
43
+ }
44
+ const bodyErrors = Array.isArray(body?.errors) ? body.errors : [];
45
+ if (bodyErrors.length > 0) {
46
+ return {
47
+ httpStatus,
48
+ data: body?.data ?? null,
49
+ errors: bodyErrors.map((error) => ({
50
+ ...error,
51
+ message: error?.message ?? '',
52
+ extensions: { ...error?.extensions, httpStatus },
53
+ })),
54
+ };
55
+ }
56
+ if (!response.ok || !body) {
57
+ const text = response.statusText ? `: ${response.statusText}` : '';
58
+ return {
59
+ httpStatus,
60
+ data: null,
61
+ errors: [
62
+ {
63
+ message: response.ok ? `HTTP ${httpStatus}${text} (the body was not JSON)` : `HTTP ${httpStatus}${text}`,
64
+ extensions: { code: 'HTTP_ERROR', httpStatus },
65
+ },
66
+ ],
67
+ };
68
+ }
69
+ return { httpStatus, data: body.data ?? null, errors: [] };
70
+ }
71
+ /**
72
+ * An ORM adapter over `postGraphQL`: the SDK clients take it in place of their
73
+ * FetchAdapter, and their failures then carry the status and code.
74
+ */
75
+ export function statusAdapter(endpoint, headers, fetchFn) {
76
+ let current = { ...headers };
77
+ return {
78
+ async execute(document, variables) {
79
+ const result = await postGraphQL({ endpoint, headers: current, query: document, variables, fetchFn });
80
+ return result.errors.length > 0
81
+ ? { ok: false, data: null, errors: result.errors }
82
+ : { ok: true, data: result.data, errors: undefined };
83
+ },
84
+ setHeaders(next) {
85
+ current = { ...current, ...next };
86
+ },
87
+ getEndpoint: () => endpoint,
88
+ };
89
+ }
90
+ const codeOf = (error) => {
91
+ const code = error?.extensions?.code;
92
+ return typeof code === 'string' ? code : undefined;
93
+ };
94
+ // The status an error arrived with: ours from extensions, or the SDK
95
+ // adapter's `HTTP 503: …` message.
96
+ const statusOf = (error) => {
97
+ const status = error?.extensions?.httpStatus;
98
+ if (typeof status === 'number')
99
+ return status;
100
+ const match = /^HTTP (\d{3})\b/.exec(error?.message ?? '');
101
+ return match ? Number(match[1]) : undefined;
102
+ };
103
+ const AUTH_MESSAGE_RE = /jwt|unauthenticated|invalid token/i;
104
+ const FORBIDDEN_MESSAGE_RE = /permission denied/i;
105
+ /**
106
+ * What a failed backend answer says, first match wins: the database is
107
+ * suspended or gone (`ACCESS_SUSPENDED`), no response, the backend failing
108
+ * (5xx, 429), a rejected credential (401), a refusal (403), or anything else.
109
+ */
110
+ export function classifyBackendErrors(errors) {
111
+ const list = errors ?? [];
112
+ const some = (test) => list.some((error) => test(error));
113
+ if (some((e) => codeOf(e) === 'ACCESS_SUSPENDED'))
114
+ return 'suspended';
115
+ if (some((e) => codeOf(e) === 'NETWORK'))
116
+ return 'network';
117
+ if (some((e) => {
118
+ const status = statusOf(e);
119
+ return status !== undefined && (status >= 500 || status === 429);
120
+ })) {
121
+ return 'server';
122
+ }
123
+ if (some((e) => statusOf(e) === 401 || codeOf(e) === 'UNAUTHENTICATED' || AUTH_MESSAGE_RE.test(e?.message ?? ''))) {
124
+ return 'auth';
125
+ }
126
+ if (some((e) => statusOf(e) === 403 || codeOf(e) === 'FORBIDDEN' || FORBIDDEN_MESSAGE_RE.test(e?.message ?? ''))) {
127
+ return 'forbidden';
128
+ }
129
+ return 'graphql';
130
+ }
131
+ /** The messages of a failed answer, joined for a reason string. */
132
+ export function errorsText(errors) {
133
+ return (errors ?? []).map((error) => error?.message || codeOf(error) || 'unknown error').join('; ');
134
+ }
package/esm/host.d.ts CHANGED
@@ -74,6 +74,23 @@ export type StepUpRequest = {
74
74
  databaseName: string;
75
75
  apiEndpoint: string;
76
76
  };
77
+ /**
78
+ * The database a project is bound to, handed to the host before provisioning
79
+ * writes it to the project's `.env`, so a host that keeps its own record of
80
+ * the binding never ends up with a `.env` it doesn't know about.
81
+ */
82
+ export type ProjectBinding = {
83
+ cwd: string;
84
+ databaseId: string;
85
+ databaseName: string;
86
+ ownerId: string;
87
+ apiEndpoint: string;
88
+ modulesEndpoint: string;
89
+ /** The database this binding replaced (replace mode). */
90
+ replaces?: string;
91
+ /** True when the existing binding was kept: nothing was provisioned. */
92
+ skipped?: boolean;
93
+ };
77
94
  export interface ToolsHost {
78
95
  /** Signed-in platform account, or null/undefined when signed out. */
79
96
  account(): HostAccount | null | undefined;
@@ -91,6 +108,13 @@ export interface ToolsHost {
91
108
  previewToken?(): Promise<PreviewToken | null>;
92
109
  /** Treat tokens expiring within this window as already expired. Default 30s. */
93
110
  dataTokenSkewMs?: number;
111
+ /**
112
+ * How long provision_database waits for a provision ticket to complete.
113
+ * Default 240s, which keeps an agent tool call under a 5-minute tool
114
+ * timeout; a host that calls the tool outside such a timeout (e.g. a
115
+ * pipeline) can wait longer for a cold provision on a loaded backend.
116
+ */
117
+ provisionPollDeadlineMs?: number;
94
118
  /**
95
119
  * Optional provision overlay: pick a base preset and/or layer module
96
120
  * add/remove on top of it. The base module list always comes from the pinned
@@ -109,7 +133,29 @@ export interface ToolsHost {
109
133
  * Required for create_api_key — without it the tool refuses to mint.
110
134
  */
111
135
  deliverSecret?(delivery: SecretDelivery): Promise<void>;
136
+ /**
137
+ * What to tell the agent to do about a database that is gone or belongs to
138
+ * another account, in place of the default advice to call
139
+ * provision_database with `reprovision: true`.
140
+ */
141
+ reprovisionHint?: string;
142
+ /**
143
+ * Whether the agent may replace a binding through provision_database's
144
+ * `reprovision: true`. Default true. A host that replaces databases only
145
+ * after the user approves (calling `provisionDatabase` in replace mode
146
+ * itself) sets false, and the tool then refuses.
147
+ */
148
+ toolReprovision?: boolean;
149
+ /**
150
+ * Save a binding in the host's own store. Provisioning calls it before
151
+ * writing `.env`, on a new binding and on a kept one (`skipped`). If it
152
+ * throws, `.env` is left alone, the provision ticket stays recorded, and
153
+ * the call fails with `binding-not-saved`.
154
+ */
155
+ persistBinding?(binding: ProjectBinding): Promise<void>;
112
156
  }
113
157
  export declare const DEFAULT_DATA_TOKEN_SKEW_MS = 30000;
158
+ /** The advice for a database that is gone or under another account. */
159
+ export declare function reprovisionAdvice(host: ToolsHost): string;
114
160
  export declare function configureHost(host: ToolsHost): void;
115
161
  export declare function getHost(): ToolsHost;
package/esm/host.js CHANGED
@@ -11,6 +11,15 @@
11
11
  * on behalf of, so the same contract serves whichever adapter runs the tools.
12
12
  */
13
13
  export const DEFAULT_DATA_TOKEN_SKEW_MS = 30_000;
14
+ const DEFAULT_REPROVISION_ADVICE = 'Re-provision with provision_database (reprovision: true): the schema is rebuilt from the project, and records do not carry over.';
15
+ /** The advice for a database that is gone or under another account. */
16
+ export function reprovisionAdvice(host) {
17
+ if (host.reprovisionHint)
18
+ return host.reprovisionHint;
19
+ if (host.toolReprovision === false)
20
+ return 'Ask the user to re-provision it from the app.';
21
+ return DEFAULT_REPROVISION_ADVICE;
22
+ }
14
23
  let currentHost = null;
15
24
  export function configureHost(host) {
16
25
  currentHost = host;
package/esm/index.d.ts CHANGED
@@ -10,12 +10,21 @@ import { type ToolsHost } from './host';
10
10
  export declare const constructiveDbTools: readonly AnyHarnessTool[];
11
11
  /** Configure the host and get the tools in one call. */
12
12
  export declare function createConstructiveDbTools(host: ToolsHost): readonly AnyHarnessTool[];
13
- export { CONTEXT_ENV_KEYS, CONTEXT_ENV_PREFIX, type ContextEnvKey, type ContextSource, deriveSubdomainEndpoint, fromEnvFile, fromEnvironment, type ModulesClient, type ProjectContext, type ProjectContextFailureCode, resolveDataToken, resolveProjectContext, } from './context';
13
+ export { BlueprintBuildError, buildBlueprint, CONSTRUCTION_DEADLINE_MS, type ConstructionTableMap, readConstructionFailure, waitForConstruction, type WaitForConstructionOptions, } from './blueprint-construction';
14
+ export { CONTEXT_ENV_KEYS, CONTEXT_ENV_PREFIX, type ContextEnvKey, type ContextSource, deriveSubdomainEndpoint, fromEnvFile, fromEnvironment, type ModulesClient, type ProjectContext, type ProjectContextFailureCode, resolveDataToken, resolveProjectContext, type ResolveResult, } from './context';
15
+ export { type DatabaseProbe, probeBoundDatabase, probeDatabase, type UnreachableCause, } from './db-probe';
14
16
  export { constructiveGateDeps, type ConstructiveGateResolvers } from './gate';
15
- export { type ActiveDataToken, configureHost, type DataAuthBroker, DEFAULT_DATA_TOKEN_SKEW_MS, getHost, type HostAccount, type HostBackendConfig, type HostProvisionOverlay, type PreviewToken, type SecretDelivery, type StepUpRequest, type ToolsHost, } from './host';
17
+ export { type BackendErrorKind, classifyBackendErrors, type HttpGraphQLResponse, postGraphQL, statusAdapter, } from './graphql-http';
18
+ export { type ActiveDataToken, configureHost, type DataAuthBroker, DEFAULT_DATA_TOKEN_SKEW_MS, getHost, type HostAccount, type HostBackendConfig, type HostProvisionOverlay, type PreviewToken, type ProjectBinding, type SecretDelivery, type StepUpRequest, type ToolsHost, } from './host';
19
+ export { archiveBindingKeys, ARCHIVED_BINDING_KEYS, type EnvVars, mergeEnv, provisionEnvVars, readEnv, } from './provision-database/env-file';
16
20
  export { loadProvisionManifest, parseProvisionManifest, PROVISION_MANIFEST_FILE, type ProvisionManifest, } from './provision-database/manifest';
17
21
  export { allModulePresets, DEFAULT_PROVISION_PRESET, getModulePreset, type ModulePreset, type ProvisionModule, } from './provision-database/presets';
22
+ export { provisionDatabase, type ProvisionDatabaseDetails, type ProvisionFailureCode, type ProvisionInput, } from './provision-database/provision';
18
23
  export { moduleKey, type ProvisionOverlay, resolveProvisionModules, } from './provision-database/resolve';
24
+ export { accessOf, type ColumnDefault, type FkAction, type ModelColumn, type ModelForeignKey, type ModelPolicy, type ModelTable, type ObjectCategory, readSchemaModel, type SchemaModel, SchemaReadError, type TableAccess, } from './schema-model';
19
25
  export { toolSchema } from './tool-schema';
26
+ export { type AddRecordsDetails } from './tools/add-records';
27
+ export { type CreateApiKeyDetails } from './tools/create-api-key';
28
+ export { type RunCodegenDetails, type RunCodegenTimings } from './tools/run-codegen';
20
29
  export { createTemplatePreviewTables } from './tools/templates';
21
30
  export default constructiveDbTools;
package/esm/index.js CHANGED
@@ -42,12 +42,18 @@ export function createConstructiveDbTools(host) {
42
42
  configureHost(host);
43
43
  return constructiveDbTools;
44
44
  }
45
+ export { BlueprintBuildError, buildBlueprint, CONSTRUCTION_DEADLINE_MS, readConstructionFailure, waitForConstruction, } from './blueprint-construction';
45
46
  export { CONTEXT_ENV_KEYS, CONTEXT_ENV_PREFIX, deriveSubdomainEndpoint, fromEnvFile, fromEnvironment, resolveDataToken, resolveProjectContext, } from './context';
47
+ export { probeBoundDatabase, probeDatabase, } from './db-probe';
46
48
  export { constructiveGateDeps } from './gate';
49
+ export { classifyBackendErrors, postGraphQL, statusAdapter, } from './graphql-http';
47
50
  export { configureHost, DEFAULT_DATA_TOKEN_SKEW_MS, getHost, } from './host';
51
+ export { archiveBindingKeys, ARCHIVED_BINDING_KEYS, mergeEnv, provisionEnvVars, readEnv, } from './provision-database/env-file';
48
52
  export { loadProvisionManifest, parseProvisionManifest, PROVISION_MANIFEST_FILE, } from './provision-database/manifest';
49
53
  export { allModulePresets, DEFAULT_PROVISION_PRESET, getModulePreset, } from './provision-database/presets';
54
+ export { provisionDatabase, } from './provision-database/provision';
50
55
  export { moduleKey, resolveProvisionModules, } from './provision-database/resolve';
56
+ export { accessOf, readSchemaModel, SchemaReadError, } from './schema-model';
51
57
  export { toolSchema } from './tool-schema';
52
58
  export { createTemplatePreviewTables } from './tools/templates';
53
59
  export default constructiveDbTools;
@@ -1,10 +1,31 @@
1
1
  export type EnvVars = Record<string, string>;
2
+ /** Parse .env text: quoted values, `export` prefixes and comments; last definition wins. */
3
+ export declare function readEnv(content: string): EnvVars;
2
4
  /**
3
- * Upsert `vars` into the existing `.env` `content`. Existing keys are replaced
4
- * in place (preserving surrounding lines/comments); new keys are appended.
5
+ * Upsert `vars` into the existing `.env` `content`. An existing key is
6
+ * replaced in place (keeping an `export` prefix), and any later definition of
7
+ * it is dropped so the new value is the one that wins; new keys are appended.
5
8
  * Returns the new file body, trailing-newline-terminated.
6
9
  */
7
10
  export declare function mergeEnv(content: string, vars: EnvVars): string;
11
+ /**
12
+ * Drop every definition of `keys`. Comments and other entries pass through,
13
+ * and content without those keys comes back unchanged.
14
+ */
15
+ export declare function removeEnvKeys(content: string, keys: readonly string[]): string;
16
+ /**
17
+ * The provision request provision_database is still waiting on, recorded when
18
+ * it stops waiting so the next call resumes that ticket. It's resumed only
19
+ * for the same database name, backend and account. Absent whenever no request
20
+ * is pending.
21
+ */
22
+ export declare const PENDING_PROVISION_KEYS: {
23
+ readonly ticketId: "PROVISION_TICKET_ID";
24
+ readonly databaseName: "PROVISION_TICKET_DATABASE_NAME";
25
+ readonly apiEndpoint: "PROVISION_TICKET_API_ENDPOINT";
26
+ readonly userId: "PROVISION_TICKET_USER_ID";
27
+ readonly attempts: "PROVISION_TICKET_ATTEMPTS";
28
+ };
8
29
  /** The credential keys provision_database owns in a project's .env. */
9
30
  export declare const PROVISION_ENV_KEYS: readonly ["DATABASE_ID", "DATABASE_NAME", "OWNER_ID", "ACCESS_TOKEN", "PGDATABASE", "API_ENDPOINT", "MODULES_ENDPOINT"];
10
31
  /**