@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.
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,5 +1,6 @@
1
1
  import { filterInternalPolicies } from '@agentic-kit/harness';
2
2
  import { z } from 'zod';
3
+ import { buildBlueprint } from '../blueprint-construction';
3
4
  import { resolveProjectContext } from '../context';
4
5
  // A stored blueprint field's `type` is an expanded object (`{ name: 'text' }`),
5
6
  // not the bare string the agent passes to provision_blueprint. Flatten it to the
@@ -252,21 +253,8 @@ export const applyTemplateTool = {
252
253
  const blueprintId = copyResult.copyTemplateToBlueprint?.result;
253
254
  if (!blueprintId)
254
255
  throw new Error('Failed to copy template');
255
- const constructResult = await modules.mutation
256
- .constructBlueprint({ input: { blueprintId, schemaId } }, { select: { result: true } })
257
- .unwrap();
258
- const constructionId = constructResult.constructBlueprint?.result;
259
- if (!constructionId)
260
- throw new Error('constructBlueprint returned no construction id');
261
- const constructions = await modules.blueprintConstruction
262
- .findMany({
263
- where: { id: { equalTo: constructionId } },
264
- first: 1,
265
- select: { tableMap: true },
266
- })
267
- .unwrap();
268
- const tableMap = (constructions?.blueprintConstructions?.nodes?.[0]?.tableMap ?? {});
269
- const createdCount = Object.keys(tableMap).length;
256
+ const { tables } = await buildBlueprint(modules, { blueprintId, schemaId }, { signal: ctx.signal });
257
+ const createdCount = Object.keys(tables).length;
270
258
  return done(true, `Applied template "${params.templateName}" — created ${createdCount} table${createdCount === 1 ? '' : 's'}`);
271
259
  }
272
260
  catch (err) {
@@ -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,140 @@
1
+ "use strict";
2
+ // A GraphQL POST that keeps what the SDK's FetchAdapter throws away. That
3
+ // adapter turns any non-2xx into `HTTP 403: Forbidden` and drops the body, so
4
+ // a tenant API's `ACCESS_SUSPENDED` (a 403 with a JSON body, sent for a
5
+ // deleted or suspended database) reads the same as any other refusal. Here
6
+ // the body's errors keep their `extensions.code`, and every error carries the
7
+ // HTTP status it arrived with.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.postGraphQL = postGraphQL;
10
+ exports.statusAdapter = statusAdapter;
11
+ exports.classifyBackendErrors = classifyBackendErrors;
12
+ exports.errorsText = errorsText;
13
+ const runtime_1 = require("@constructive-io/graphql-query/runtime");
14
+ const errorText = (err) => (err instanceof Error ? err.message : String(err));
15
+ async function postGraphQL(args) {
16
+ const fetchFn = (args.fetchFn ?? (0, runtime_1.createFetch)()).bind(globalThis);
17
+ let response;
18
+ try {
19
+ response = await fetchFn(args.endpoint, {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ Accept: 'application/json',
24
+ ...args.headers,
25
+ },
26
+ body: JSON.stringify({ query: args.query, variables: args.variables ?? {} }),
27
+ });
28
+ }
29
+ catch (err) {
30
+ const cause = err?.cause?.code;
31
+ return {
32
+ httpStatus: null,
33
+ data: null,
34
+ errors: [
35
+ {
36
+ message: typeof cause === 'string' ? `${errorText(err)} (${cause})` : errorText(err),
37
+ extensions: { code: 'NETWORK' },
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ const httpStatus = response.status;
43
+ let body = null;
44
+ try {
45
+ body = (await response.json());
46
+ }
47
+ catch {
48
+ body = null;
49
+ }
50
+ const bodyErrors = Array.isArray(body?.errors) ? body.errors : [];
51
+ if (bodyErrors.length > 0) {
52
+ return {
53
+ httpStatus,
54
+ data: body?.data ?? null,
55
+ errors: bodyErrors.map((error) => ({
56
+ ...error,
57
+ message: error?.message ?? '',
58
+ extensions: { ...error?.extensions, httpStatus },
59
+ })),
60
+ };
61
+ }
62
+ if (!response.ok || !body) {
63
+ const text = response.statusText ? `: ${response.statusText}` : '';
64
+ return {
65
+ httpStatus,
66
+ data: null,
67
+ errors: [
68
+ {
69
+ message: response.ok ? `HTTP ${httpStatus}${text} (the body was not JSON)` : `HTTP ${httpStatus}${text}`,
70
+ extensions: { code: 'HTTP_ERROR', httpStatus },
71
+ },
72
+ ],
73
+ };
74
+ }
75
+ return { httpStatus, data: body.data ?? null, errors: [] };
76
+ }
77
+ /**
78
+ * An ORM adapter over `postGraphQL`: the SDK clients take it in place of their
79
+ * FetchAdapter, and their failures then carry the status and code.
80
+ */
81
+ function statusAdapter(endpoint, headers, fetchFn) {
82
+ let current = { ...headers };
83
+ return {
84
+ async execute(document, variables) {
85
+ const result = await postGraphQL({ endpoint, headers: current, query: document, variables, fetchFn });
86
+ return result.errors.length > 0
87
+ ? { ok: false, data: null, errors: result.errors }
88
+ : { ok: true, data: result.data, errors: undefined };
89
+ },
90
+ setHeaders(next) {
91
+ current = { ...current, ...next };
92
+ },
93
+ getEndpoint: () => endpoint,
94
+ };
95
+ }
96
+ const codeOf = (error) => {
97
+ const code = error?.extensions?.code;
98
+ return typeof code === 'string' ? code : undefined;
99
+ };
100
+ // The status an error arrived with: ours from extensions, or the SDK
101
+ // adapter's `HTTP 503: …` message.
102
+ const statusOf = (error) => {
103
+ const status = error?.extensions?.httpStatus;
104
+ if (typeof status === 'number')
105
+ return status;
106
+ const match = /^HTTP (\d{3})\b/.exec(error?.message ?? '');
107
+ return match ? Number(match[1]) : undefined;
108
+ };
109
+ const AUTH_MESSAGE_RE = /jwt|unauthenticated|invalid token/i;
110
+ const FORBIDDEN_MESSAGE_RE = /permission denied/i;
111
+ /**
112
+ * What a failed backend answer says, first match wins: the database is
113
+ * suspended or gone (`ACCESS_SUSPENDED`), no response, the backend failing
114
+ * (5xx, 429), a rejected credential (401), a refusal (403), or anything else.
115
+ */
116
+ function classifyBackendErrors(errors) {
117
+ const list = errors ?? [];
118
+ const some = (test) => list.some((error) => test(error));
119
+ if (some((e) => codeOf(e) === 'ACCESS_SUSPENDED'))
120
+ return 'suspended';
121
+ if (some((e) => codeOf(e) === 'NETWORK'))
122
+ return 'network';
123
+ if (some((e) => {
124
+ const status = statusOf(e);
125
+ return status !== undefined && (status >= 500 || status === 429);
126
+ })) {
127
+ return 'server';
128
+ }
129
+ if (some((e) => statusOf(e) === 401 || codeOf(e) === 'UNAUTHENTICATED' || AUTH_MESSAGE_RE.test(e?.message ?? ''))) {
130
+ return 'auth';
131
+ }
132
+ if (some((e) => statusOf(e) === 403 || codeOf(e) === 'FORBIDDEN' || FORBIDDEN_MESSAGE_RE.test(e?.message ?? ''))) {
133
+ return 'forbidden';
134
+ }
135
+ return 'graphql';
136
+ }
137
+ /** The messages of a failed answer, joined for a reason string. */
138
+ function errorsText(errors) {
139
+ return (errors ?? []).map((error) => error?.message || codeOf(error) || 'unknown error').join('; ');
140
+ }
package/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/host.js CHANGED
@@ -13,9 +13,19 @@
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.DEFAULT_DATA_TOKEN_SKEW_MS = void 0;
16
+ exports.reprovisionAdvice = reprovisionAdvice;
16
17
  exports.configureHost = configureHost;
17
18
  exports.getHost = getHost;
18
19
  exports.DEFAULT_DATA_TOKEN_SKEW_MS = 30_000;
20
+ const DEFAULT_REPROVISION_ADVICE = 'Re-provision with provision_database (reprovision: true): the schema is rebuilt from the project, and records do not carry over.';
21
+ /** The advice for a database that is gone or under another account. */
22
+ function reprovisionAdvice(host) {
23
+ if (host.reprovisionHint)
24
+ return host.reprovisionHint;
25
+ if (host.toolReprovision === false)
26
+ return 'Ask the user to re-provision it from the app.';
27
+ return DEFAULT_REPROVISION_ADVICE;
28
+ }
19
29
  let currentHost = null;
20
30
  function configureHost(host) {
21
31
  currentHost = host;
package/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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createTemplatePreviewTables = exports.toolSchema = exports.resolveProvisionModules = exports.moduleKey = exports.getModulePreset = exports.DEFAULT_PROVISION_PRESET = exports.allModulePresets = exports.PROVISION_MANIFEST_FILE = exports.parseProvisionManifest = exports.loadProvisionManifest = exports.getHost = exports.DEFAULT_DATA_TOKEN_SKEW_MS = exports.configureHost = exports.constructiveGateDeps = exports.resolveProjectContext = exports.resolveDataToken = exports.fromEnvironment = exports.fromEnvFile = exports.deriveSubdomainEndpoint = exports.CONTEXT_ENV_PREFIX = exports.CONTEXT_ENV_KEYS = exports.constructiveDbTools = void 0;
3
+ exports.createTemplatePreviewTables = exports.toolSchema = exports.SchemaReadError = exports.readSchemaModel = exports.accessOf = exports.resolveProvisionModules = exports.moduleKey = exports.provisionDatabase = exports.getModulePreset = exports.DEFAULT_PROVISION_PRESET = exports.allModulePresets = exports.PROVISION_MANIFEST_FILE = exports.parseProvisionManifest = exports.loadProvisionManifest = exports.readEnv = exports.provisionEnvVars = exports.mergeEnv = exports.ARCHIVED_BINDING_KEYS = exports.archiveBindingKeys = exports.getHost = exports.DEFAULT_DATA_TOKEN_SKEW_MS = exports.configureHost = exports.statusAdapter = exports.postGraphQL = exports.classifyBackendErrors = exports.constructiveGateDeps = exports.probeDatabase = exports.probeBoundDatabase = exports.resolveProjectContext = exports.resolveDataToken = exports.fromEnvironment = exports.fromEnvFile = exports.deriveSubdomainEndpoint = exports.CONTEXT_ENV_PREFIX = exports.CONTEXT_ENV_KEYS = exports.waitForConstruction = exports.readConstructionFailure = exports.CONSTRUCTION_DEADLINE_MS = exports.buildBlueprint = exports.BlueprintBuildError = exports.constructiveDbTools = void 0;
4
4
  exports.createConstructiveDbTools = createConstructiveDbTools;
5
5
  const host_1 = require("./host");
6
6
  const add_policies_1 = require("./tools/add-policies");
@@ -46,6 +46,12 @@ function createConstructiveDbTools(host) {
46
46
  (0, host_1.configureHost)(host);
47
47
  return exports.constructiveDbTools;
48
48
  }
49
+ var blueprint_construction_1 = require("./blueprint-construction");
50
+ Object.defineProperty(exports, "BlueprintBuildError", { enumerable: true, get: function () { return blueprint_construction_1.BlueprintBuildError; } });
51
+ Object.defineProperty(exports, "buildBlueprint", { enumerable: true, get: function () { return blueprint_construction_1.buildBlueprint; } });
52
+ Object.defineProperty(exports, "CONSTRUCTION_DEADLINE_MS", { enumerable: true, get: function () { return blueprint_construction_1.CONSTRUCTION_DEADLINE_MS; } });
53
+ Object.defineProperty(exports, "readConstructionFailure", { enumerable: true, get: function () { return blueprint_construction_1.readConstructionFailure; } });
54
+ Object.defineProperty(exports, "waitForConstruction", { enumerable: true, get: function () { return blueprint_construction_1.waitForConstruction; } });
49
55
  var context_1 = require("./context");
50
56
  Object.defineProperty(exports, "CONTEXT_ENV_KEYS", { enumerable: true, get: function () { return context_1.CONTEXT_ENV_KEYS; } });
51
57
  Object.defineProperty(exports, "CONTEXT_ENV_PREFIX", { enumerable: true, get: function () { return context_1.CONTEXT_ENV_PREFIX; } });
@@ -54,12 +60,25 @@ Object.defineProperty(exports, "fromEnvFile", { enumerable: true, get: function
54
60
  Object.defineProperty(exports, "fromEnvironment", { enumerable: true, get: function () { return context_1.fromEnvironment; } });
55
61
  Object.defineProperty(exports, "resolveDataToken", { enumerable: true, get: function () { return context_1.resolveDataToken; } });
56
62
  Object.defineProperty(exports, "resolveProjectContext", { enumerable: true, get: function () { return context_1.resolveProjectContext; } });
63
+ var db_probe_1 = require("./db-probe");
64
+ Object.defineProperty(exports, "probeBoundDatabase", { enumerable: true, get: function () { return db_probe_1.probeBoundDatabase; } });
65
+ Object.defineProperty(exports, "probeDatabase", { enumerable: true, get: function () { return db_probe_1.probeDatabase; } });
57
66
  var gate_1 = require("./gate");
58
67
  Object.defineProperty(exports, "constructiveGateDeps", { enumerable: true, get: function () { return gate_1.constructiveGateDeps; } });
68
+ var graphql_http_1 = require("./graphql-http");
69
+ Object.defineProperty(exports, "classifyBackendErrors", { enumerable: true, get: function () { return graphql_http_1.classifyBackendErrors; } });
70
+ Object.defineProperty(exports, "postGraphQL", { enumerable: true, get: function () { return graphql_http_1.postGraphQL; } });
71
+ Object.defineProperty(exports, "statusAdapter", { enumerable: true, get: function () { return graphql_http_1.statusAdapter; } });
59
72
  var host_2 = require("./host");
60
73
  Object.defineProperty(exports, "configureHost", { enumerable: true, get: function () { return host_2.configureHost; } });
61
74
  Object.defineProperty(exports, "DEFAULT_DATA_TOKEN_SKEW_MS", { enumerable: true, get: function () { return host_2.DEFAULT_DATA_TOKEN_SKEW_MS; } });
62
75
  Object.defineProperty(exports, "getHost", { enumerable: true, get: function () { return host_2.getHost; } });
76
+ var env_file_1 = require("./provision-database/env-file");
77
+ Object.defineProperty(exports, "archiveBindingKeys", { enumerable: true, get: function () { return env_file_1.archiveBindingKeys; } });
78
+ Object.defineProperty(exports, "ARCHIVED_BINDING_KEYS", { enumerable: true, get: function () { return env_file_1.ARCHIVED_BINDING_KEYS; } });
79
+ Object.defineProperty(exports, "mergeEnv", { enumerable: true, get: function () { return env_file_1.mergeEnv; } });
80
+ Object.defineProperty(exports, "provisionEnvVars", { enumerable: true, get: function () { return env_file_1.provisionEnvVars; } });
81
+ Object.defineProperty(exports, "readEnv", { enumerable: true, get: function () { return env_file_1.readEnv; } });
63
82
  var manifest_1 = require("./provision-database/manifest");
64
83
  Object.defineProperty(exports, "loadProvisionManifest", { enumerable: true, get: function () { return manifest_1.loadProvisionManifest; } });
65
84
  Object.defineProperty(exports, "parseProvisionManifest", { enumerable: true, get: function () { return manifest_1.parseProvisionManifest; } });
@@ -68,9 +87,15 @@ var presets_1 = require("./provision-database/presets");
68
87
  Object.defineProperty(exports, "allModulePresets", { enumerable: true, get: function () { return presets_1.allModulePresets; } });
69
88
  Object.defineProperty(exports, "DEFAULT_PROVISION_PRESET", { enumerable: true, get: function () { return presets_1.DEFAULT_PROVISION_PRESET; } });
70
89
  Object.defineProperty(exports, "getModulePreset", { enumerable: true, get: function () { return presets_1.getModulePreset; } });
90
+ var provision_1 = require("./provision-database/provision");
91
+ Object.defineProperty(exports, "provisionDatabase", { enumerable: true, get: function () { return provision_1.provisionDatabase; } });
71
92
  var resolve_1 = require("./provision-database/resolve");
72
93
  Object.defineProperty(exports, "moduleKey", { enumerable: true, get: function () { return resolve_1.moduleKey; } });
73
94
  Object.defineProperty(exports, "resolveProvisionModules", { enumerable: true, get: function () { return resolve_1.resolveProvisionModules; } });
95
+ var schema_model_1 = require("./schema-model");
96
+ Object.defineProperty(exports, "accessOf", { enumerable: true, get: function () { return schema_model_1.accessOf; } });
97
+ Object.defineProperty(exports, "readSchemaModel", { enumerable: true, get: function () { return schema_model_1.readSchemaModel; } });
98
+ Object.defineProperty(exports, "SchemaReadError", { enumerable: true, get: function () { return schema_model_1.SchemaReadError; } });
74
99
  var tool_schema_1 = require("./tool-schema");
75
100
  Object.defineProperty(exports, "toolSchema", { enumerable: true, get: function () { return tool_schema_1.toolSchema; } });
76
101
  var templates_2 = require("./tools/templates");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentic-kit/db-tools",
3
- "version": "0.11.4",
3
+ "version": "0.12.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "harness-neutral Constructive database tools — provisioning, schema, policies, records, templates and codegen as HarnessTools any adapter can bind",
6
6
  "main": "index.js",
@@ -31,8 +31,8 @@
31
31
  "dependencies": {
32
32
  "12factor-env": "^1.34.0",
33
33
  "@agentic-kit/harness": "^0.18.0",
34
- "@constructive-io/graphql-query": "^4.22.4",
35
- "@constructive-io/sdk": "^1.21.4",
34
+ "@constructive-io/graphql-query": "^4.22.6",
35
+ "@constructive-io/sdk": "^1.21.6",
36
36
  "node-type-registry": "^1.22.0",
37
37
  "zod": "^4.4.3"
38
38
  },
@@ -45,5 +45,5 @@
45
45
  "database",
46
46
  "constructive"
47
47
  ],
48
- "gitHead": "ed0726630bf8211ff3f58934e0e0b70a180284db"
48
+ "gitHead": "124df300f0afc15653854b50a25ac738c70261b7"
49
49
  }
@@ -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
  /**
@@ -1,32 +1,100 @@
1
1
  "use strict";
2
- // Pure .env merge — used by provision_database to persist provisioning
3
- // credentials without clobbering existing keys: replace a key in place if
4
- // present, else append.
2
+ // Pure .env edits — used by provision_database to persist provisioning
3
+ // credentials without clobbering existing keys.
5
4
  // Kept side-effect-free so it is unit-testable (the tool does the fs write).
5
+ //
6
+ // Reads go through the same parser context.ts uses (quotes, `export`,
7
+ // comments, the last definition wins), and edits find a key the way that
8
+ // parser does, so a key written as `export KEY="value"` or spread over several
9
+ // lines by a quoted value is still the key being replaced or removed.
6
10
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.ARCHIVED_BINDING_KEYS = exports.PROVISION_ENV_KEYS = void 0;
11
+ exports.ARCHIVED_BINDING_KEYS = exports.PROVISION_ENV_KEYS = exports.PENDING_PROVISION_KEYS = void 0;
12
+ exports.readEnv = readEnv;
8
13
  exports.mergeEnv = mergeEnv;
14
+ exports.removeEnvKeys = removeEnvKeys;
9
15
  exports.provisionEnvVars = provisionEnvVars;
10
16
  exports.archiveBindingKeys = archiveBindingKeys;
17
+ const dotenv_1 = require("12factor-env/dotenv");
18
+ /** Parse .env text: quoted values, `export` prefixes and comments; last definition wins. */
19
+ function readEnv(content) {
20
+ return (0, dotenv_1.parseDotenv)(content);
21
+ }
22
+ const ENTRY_RE = /^(\s*)(export\s+)?([\w.-]+)\s*=\s*(.*)$/;
23
+ const QUOTES = new Set(['"', "'", '`']);
24
+ // Group lines into entries: a key with the lines its value spans (a quoted
25
+ // value left open continues to the line that closes it), or a non-entry line
26
+ // (comment, blank, anything else) with a null key.
27
+ function envEntries(content) {
28
+ const lines = content.split('\n');
29
+ const entries = [];
30
+ for (let i = 0; i < lines.length; i++) {
31
+ const line = lines[i];
32
+ const match = line.trimStart().startsWith('#') ? null : ENTRY_RE.exec(line);
33
+ if (!match) {
34
+ entries.push({ key: null, exported: false, lines: [line] });
35
+ continue;
36
+ }
37
+ const value = match[4];
38
+ const span = [line];
39
+ if (QUOTES.has(value[0]) && !value.slice(1).includes(value[0])) {
40
+ while (i + 1 < lines.length) {
41
+ span.push(lines[++i]);
42
+ if (lines[i].includes(value[0]))
43
+ break;
44
+ }
45
+ }
46
+ entries.push({ key: match[3], exported: !!match[2], lines: span });
47
+ }
48
+ return entries;
49
+ }
11
50
  /**
12
- * Upsert `vars` into the existing `.env` `content`. Existing keys are replaced
13
- * in place (preserving surrounding lines/comments); new keys are appended.
51
+ * Upsert `vars` into the existing `.env` `content`. An existing key is
52
+ * replaced in place (keeping an `export` prefix), and any later definition of
53
+ * it is dropped so the new value is the one that wins; new keys are appended.
14
54
  * Returns the new file body, trailing-newline-terminated.
15
55
  */
16
56
  function mergeEnv(content, vars) {
17
- let out = content;
18
- for (const [key, val] of Object.entries(vars)) {
19
- // Escape the key for the regex, and use a replacement function so `$`
20
- // sequences in the value are never treated as replacement specials.
21
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
- const regex = new RegExp(`^${escapedKey}=.*`, 'm');
23
- if (regex.test(out))
24
- out = out.replace(regex, () => `${key}=${val}`);
25
- else
26
- out += `\n${key}=${val}`;
57
+ const remaining = new Map(Object.entries(vars));
58
+ const written = new Set();
59
+ const out = [];
60
+ for (const entry of envEntries(content)) {
61
+ if (entry.key !== null && remaining.has(entry.key)) {
62
+ out.push(`${entry.exported ? 'export ' : ''}${entry.key}=${remaining.get(entry.key)}`);
63
+ remaining.delete(entry.key);
64
+ written.add(entry.key);
65
+ }
66
+ else if (entry.key === null || !written.has(entry.key)) {
67
+ out.push(...entry.lines);
68
+ }
27
69
  }
28
- return out.trim() + '\n';
70
+ let text = out.join('\n');
71
+ for (const [key, value] of remaining)
72
+ text += `\n${key}=${value}`;
73
+ return text.trim() + '\n';
29
74
  }
75
+ /**
76
+ * Drop every definition of `keys`. Comments and other entries pass through,
77
+ * and content without those keys comes back unchanged.
78
+ */
79
+ function removeEnvKeys(content, keys) {
80
+ const keySet = new Set(keys);
81
+ const entries = envEntries(content);
82
+ const kept = entries.filter((entry) => entry.key === null || !keySet.has(entry.key));
83
+ return kept.length === entries.length ? content : kept.flatMap((entry) => entry.lines).join('\n');
84
+ }
85
+ /**
86
+ * The provision request provision_database is still waiting on, recorded when
87
+ * it stops waiting so the next call resumes that ticket. It's resumed only
88
+ * for the same database name, backend and account. Absent whenever no request
89
+ * is pending.
90
+ */
91
+ exports.PENDING_PROVISION_KEYS = {
92
+ ticketId: 'PROVISION_TICKET_ID',
93
+ databaseName: 'PROVISION_TICKET_DATABASE_NAME',
94
+ apiEndpoint: 'PROVISION_TICKET_API_ENDPOINT',
95
+ userId: 'PROVISION_TICKET_USER_ID',
96
+ attempts: 'PROVISION_TICKET_ATTEMPTS',
97
+ };
30
98
  /** The credential keys provision_database owns in a project's .env. */
31
99
  exports.PROVISION_ENV_KEYS = [
32
100
  'DATABASE_ID',
@@ -76,16 +144,9 @@ function provisionEnvVars(args) {
76
144
  */
77
145
  function archiveBindingKeys(content, keys, archivedOn) {
78
146
  const keySet = new Set(keys);
79
- return content
80
- .split('\n')
81
- .map((line) => {
82
- if (line.trimStart().startsWith('#'))
83
- return line;
84
- const eq = line.indexOf('=');
85
- if (eq === -1)
86
- return line;
87
- const key = line.slice(0, eq).trim();
88
- return keySet.has(key) ? `# archived ${archivedOn}: ${line.trim()}` : line;
89
- })
147
+ return envEntries(content)
148
+ .flatMap((entry) => entry.key !== null && keySet.has(entry.key)
149
+ ? entry.lines.map((line) => `# archived ${archivedOn}: ${line.trim()}`)
150
+ : entry.lines)
90
151
  .join('\n');
91
152
  }
@@ -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;