@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
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ // The live schema of a project's app tables, read from the metaschema in one
3
+ // piece: every APP table with every field (whatever its category), keys,
4
+ // checks, foreign keys and policies. The ids are the metaschema's own, so they
5
+ // stay put across renames.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SchemaReadError = void 0;
8
+ exports.accessOf = accessOf;
9
+ exports.readSchemaModel = readSchemaModel;
10
+ const harness_1 = require("@agentic-kit/harness");
11
+ const graphql_http_1 = require("./graphql-http");
12
+ const normalize_1 = require("./schema-model/normalize");
13
+ /** A read failed, so there is no model: a partial one would look like columns dropped outside. */
14
+ class SchemaReadError extends Error {
15
+ step;
16
+ errors;
17
+ constructor(message, step, errors) {
18
+ super(message);
19
+ this.step = step;
20
+ this.errors = errors;
21
+ this.name = 'SchemaReadError';
22
+ }
23
+ }
24
+ exports.SchemaReadError = SchemaReadError;
25
+ // ---- access ------------------------------------------------------------
26
+ const CRUD = ['select', 'insert', 'update', 'delete'];
27
+ const OWNER_FIELD = 'owner_id';
28
+ // Keys the backend fills in when it stores an app-membership policy: where the
29
+ // membership table lives, and that it's the app's (type 1, not an org's).
30
+ const RESOLVED_KEYS = new Set(['sprt_table', 'sprt_schema']);
31
+ const APP_MEMBERSHIP_TYPE = 1;
32
+ // A parameter counts only when it narrows the policy: `is_admin: false` or
33
+ // `levels: []` is the same as leaving it out.
34
+ const isSet = (value) => Array.isArray(value)
35
+ ? value.length > 0
36
+ : typeof value === 'object' && value !== null
37
+ ? Object.keys(value).length > 0
38
+ : Boolean(value);
39
+ /** Whether a policy parameter narrows who the policy lets in. */
40
+ const narrows = (data, key) => {
41
+ if (key === 'owner_field' || RESOLVED_KEYS.has(key))
42
+ return false;
43
+ if (key === 'membership_type')
44
+ return Number(data[key]) !== APP_MEMBERSHIP_TYPE;
45
+ return isSet(data[key]);
46
+ };
47
+ function modeOf(policy) {
48
+ if (!policy.permissive || policy.role !== 'authenticated')
49
+ return null;
50
+ if (policy.withCheck && isSet(policy.withCheck))
51
+ return null;
52
+ const data = policy.data ?? {};
53
+ if (Object.keys(data).some((key) => narrows(data, key)))
54
+ return null;
55
+ if (policy.type === 'AuthzAppMembership')
56
+ return isSet(data.owner_field) ? null : 'shared';
57
+ if (policy.type === 'AuthzAppMemberOwner') {
58
+ const owner = data.owner_field;
59
+ return owner === undefined || owner === null || owner === OWNER_FIELD ? 'private' : null;
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * The access mode a table's policies amount to. `private` is the app-member
65
+ * owner policy on `owner_id` and `shared` the plain app-membership policy,
66
+ * each for the authenticated role over select, insert, update and delete.
67
+ * Anything else, no policies included, is `custom`. Disabled and internal
68
+ * policies don't count.
69
+ */
70
+ function accessOf(policies) {
71
+ const live = policies.filter((policy) => !policy.disabled && !(0, harness_1.isInternalPolicy)(policy.type));
72
+ if (live.length === 0)
73
+ return 'custom';
74
+ const modes = new Set(live.map(modeOf));
75
+ if (modes.size !== 1 || modes.has(null))
76
+ return 'custom';
77
+ const covered = new Set(live.flatMap((policy) => {
78
+ const privilege = (policy.privilege ?? '').toLowerCase();
79
+ return privilege === 'all' ? CRUD : [privilege];
80
+ }));
81
+ return CRUD.every((privilege) => covered.has(privilege)) ? [...modes][0] : 'custom';
82
+ }
83
+ const nodesOf = (connection) => (connection?.nodes ?? []).filter((node) => Boolean(node));
84
+ async function read(step, query) {
85
+ let result;
86
+ try {
87
+ result = await query.execute();
88
+ }
89
+ catch (err) {
90
+ throw new SchemaReadError(`Could not read ${step}: ${err instanceof Error ? err.message : String(err)}`, step);
91
+ }
92
+ if (!result.ok) {
93
+ throw new SchemaReadError(`Could not read ${step}: ${(0, graphql_http_1.errorsText)(result.errors ?? [])}`, step, result.errors);
94
+ }
95
+ return result.data;
96
+ }
97
+ const asRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null;
98
+ const groupBy = (rows) => {
99
+ const map = new Map();
100
+ for (const row of rows) {
101
+ const list = map.get(row.tableId) ?? [];
102
+ list.push(row);
103
+ map.set(row.tableId, list);
104
+ }
105
+ return map;
106
+ };
107
+ // The hash first: anything that changes the schema after this read moves it,
108
+ // so a caller comparing hashes later never trusts a model older than it looks.
109
+ async function readHash(context) {
110
+ try {
111
+ const data = await read('the database', context.api.database.findMany({
112
+ select: { id: true, hash: true },
113
+ where: { id: { equalTo: context.databaseId } },
114
+ first: 1,
115
+ }));
116
+ const node = nodesOf(data.databases)[0];
117
+ if (!node) {
118
+ throw new SchemaReadError(`Database ${context.databaseId} was not found on this backend.`, 'the database');
119
+ }
120
+ return node.hash ?? null;
121
+ }
122
+ catch (err) {
123
+ // A backend from before Database.hash.
124
+ if (err instanceof SchemaReadError && (err.errors ?? []).some((e) => /\bhash\b/.test(e?.message ?? ''))) {
125
+ return null;
126
+ }
127
+ throw err;
128
+ }
129
+ }
130
+ /**
131
+ * Read the APP tables of the context's database. All or nothing: any failed
132
+ * query throws `SchemaReadError`.
133
+ */
134
+ async function readSchemaModel(context) {
135
+ const { api, databaseId } = context;
136
+ const hash = await readHash(context);
137
+ // Every table of the database, so a foreign key to a non-APP table (users)
138
+ // still names its target.
139
+ const tableData = await read('tables', api.table.findMany({
140
+ select: { id: true, name: true, category: true, label: true },
141
+ where: { databaseId: { equalTo: databaseId } },
142
+ orderBy: ['NAME_ASC'],
143
+ }));
144
+ const allTables = nodesOf(tableData.tables);
145
+ const tablesById = new Map(allTables.map((table) => [table.id, table]));
146
+ const appTables = allTables
147
+ .filter((table) => table.category === 'APP')
148
+ .sort((a, b) => a.name.localeCompare(b.name));
149
+ if (appTables.length === 0)
150
+ return { databaseId, hash, tables: [] };
151
+ const inApp = { tableId: { in: appTables.map((table) => table.id) } };
152
+ const [fieldData, pkData, uniqueData, checkData, fkData, policyData] = await Promise.all([
153
+ read('fields', api.field.findMany({
154
+ select: {
155
+ id: true,
156
+ name: true,
157
+ tableId: true,
158
+ category: true,
159
+ fieldOrder: true,
160
+ type: true,
161
+ isRequired: true,
162
+ defaultValue: true,
163
+ chk: true,
164
+ chkExpr: true,
165
+ generationType: true,
166
+ identityGeneration: true,
167
+ label: true,
168
+ description: true,
169
+ smartTags: true,
170
+ },
171
+ where: inApp,
172
+ })),
173
+ read('primary keys', api.primaryKeyConstraint.findMany({
174
+ select: { id: true, tableId: true, fieldIds: true },
175
+ where: inApp,
176
+ })),
177
+ read('unique constraints', api.uniqueConstraint.findMany({
178
+ select: { id: true, tableId: true, fieldIds: true },
179
+ where: inApp,
180
+ })),
181
+ read('check constraints', api.checkConstraint.findMany({
182
+ select: { id: true, tableId: true, fieldIds: true, expr: true },
183
+ where: inApp,
184
+ })),
185
+ read('foreign keys', api.foreignKeyConstraint.findMany({
186
+ select: {
187
+ id: true,
188
+ name: true,
189
+ tableId: true,
190
+ fieldIds: true,
191
+ refTableId: true,
192
+ refFieldIds: true,
193
+ deleteAction: true,
194
+ updateAction: true,
195
+ },
196
+ where: inApp,
197
+ })),
198
+ read('policies', api.policy.findMany({
199
+ select: {
200
+ id: true,
201
+ tableId: true,
202
+ policyType: true,
203
+ privilege: true,
204
+ granteeName: true,
205
+ permissive: true,
206
+ disabled: true,
207
+ data: true,
208
+ withCheck: true,
209
+ },
210
+ where: inApp,
211
+ })),
212
+ ]);
213
+ const fields = nodesOf(fieldData.fields);
214
+ const fieldNames = new Map(fields.map((field) => [field.id, field.name]));
215
+ const foreignKeys = nodesOf(fkData.foreignKeyConstraints);
216
+ // Columns a foreign key points at outside the APP tables (users.id).
217
+ const outside = [
218
+ ...new Set(foreignKeys.flatMap((fk) => fk.refFieldIds ?? []).filter((id) => !fieldNames.has(id))),
219
+ ];
220
+ if (outside.length > 0) {
221
+ const refData = await read('referenced fields', api.field.findMany({
222
+ select: { id: true, name: true },
223
+ where: { id: { in: outside } },
224
+ }));
225
+ for (const field of nodesOf(refData.fields))
226
+ fieldNames.set(field.id, field.name);
227
+ }
228
+ const fieldsByTable = groupBy(fields);
229
+ const pksByTable = groupBy(nodesOf(pkData.primaryKeyConstraints));
230
+ const uniquesByTable = groupBy(nodesOf(uniqueData.uniqueConstraints));
231
+ const checksByTable = groupBy(nodesOf(checkData.checkConstraints));
232
+ const fksByTable = groupBy(foreignKeys);
233
+ const policiesByTable = groupBy(nodesOf(policyData.policies));
234
+ const names = (ids) => (ids ?? []).map((id) => fieldNames.get(id) ?? id);
235
+ const tables = appTables.map((table) => {
236
+ const pkIds = new Set((pksByTable.get(table.id) ?? []).flatMap((pk) => pk.fieldIds ?? []));
237
+ const uniques = uniquesByTable.get(table.id) ?? [];
238
+ const uniqueIds = new Set(uniques.filter((u) => (u.fieldIds ?? []).length === 1).map((u) => (u.fieldIds ?? [])[0]));
239
+ const checks = checksByTable.get(table.id) ?? [];
240
+ const columns = (fieldsByTable.get(table.id) ?? [])
241
+ .map((field, index) => {
242
+ const chk = asRecord(field.chk);
243
+ const sources = [
244
+ chk && 'expression' in chk ? chk.expression : field.chk,
245
+ field.chkExpr,
246
+ ...checks.filter((c) => (c.fieldIds ?? []).length === 1 && c.fieldIds[0] === field.id).map((c) => c.expr),
247
+ ].map(normalize_1.readCheck);
248
+ const allowedValues = sources.find((source) => source.values)?.values;
249
+ const hasOtherCheck = sources.some((source) => source.other);
250
+ return {
251
+ id: field.id,
252
+ name: field.name,
253
+ category: field.category,
254
+ order: typeof field.fieldOrder === 'number' ? field.fieldOrder : index,
255
+ type: (0, normalize_1.normalizeType)(field.type),
256
+ rawType: asRecord(field.type),
257
+ isRequired: Boolean(field.isRequired),
258
+ default: (0, normalize_1.columnDefault)(field.defaultValue),
259
+ ...(allowedValues ? { allowedValues } : {}),
260
+ ...(hasOtherCheck ? { hasOtherCheck } : {}),
261
+ isPrimaryKey: pkIds.has(field.id),
262
+ isUnique: uniqueIds.has(field.id),
263
+ generated: Boolean(field.generationType || field.identityGeneration),
264
+ ...(field.label ? { label: field.label } : {}),
265
+ ...(field.description ? { description: field.description } : {}),
266
+ ...(field.smartTags ? { smartTags: field.smartTags } : {}),
267
+ };
268
+ })
269
+ .sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
270
+ const policies = (policiesByTable.get(table.id) ?? [])
271
+ .filter((policy) => !(0, harness_1.isInternalPolicy)(policy.policyType))
272
+ .map((policy) => ({
273
+ id: policy.id,
274
+ type: policy.policyType ?? '',
275
+ privilege: (policy.privilege ?? '').toLowerCase(),
276
+ role: policy.granteeName ?? '',
277
+ permissive: policy.permissive !== false,
278
+ disabled: policy.disabled === true,
279
+ data: asRecord(policy.data),
280
+ withCheck: asRecord(policy.withCheck),
281
+ }));
282
+ return {
283
+ id: table.id,
284
+ name: table.name,
285
+ category: table.category,
286
+ ...(table.label ? { label: table.label } : {}),
287
+ columns,
288
+ foreignKeys: (fksByTable.get(table.id) ?? []).map((fk) => {
289
+ const target = fk.refTableId ? tablesById.get(fk.refTableId) : undefined;
290
+ return {
291
+ id: fk.id,
292
+ ...(fk.name ? { name: fk.name } : {}),
293
+ columnIds: fk.fieldIds ?? [],
294
+ columns: names(fk.fieldIds),
295
+ targetTableId: fk.refTableId ?? '',
296
+ targetTable: target?.name ?? '',
297
+ targetCategory: target?.category ?? null,
298
+ targetColumns: names(fk.refFieldIds),
299
+ onDelete: (0, normalize_1.fkAction)(fk.deleteAction),
300
+ onUpdate: (0, normalize_1.fkAction)(fk.updateAction),
301
+ };
302
+ }),
303
+ uniqueConstraints: uniques.map((u) => ({ id: u.id, columns: names(u.fieldIds) })),
304
+ access: accessOf(policies),
305
+ policies,
306
+ };
307
+ });
308
+ return { databaseId, hash, tables };
309
+ }
@@ -5,10 +5,10 @@ declare const AddPoliciesZod: z.ZodObject<{
5
5
  policies: z.ZodArray<z.ZodObject<{
6
6
  policy_type: z.ZodString;
7
7
  operations: z.ZodArray<z.ZodEnum<{
8
+ delete: "delete";
8
9
  create: "create";
9
10
  read: "read";
10
11
  update: "update";
11
- delete: "delete";
12
12
  }>>;
13
13
  role_name: z.ZodOptional<z.ZodString>;
14
14
  is_permissive: z.ZodOptional<z.ZodBoolean>;
@@ -9,6 +9,7 @@ export type AddRecordsDetails = {
9
9
  createdCount?: number;
10
10
  asOf?: string;
11
11
  needsAuth?: boolean;
12
+ code?: 'database-unavailable';
12
13
  };
13
14
  declare const AddRecordsZod: z.ZodObject<{
14
15
  table_name: z.ZodString;
@@ -2,13 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.addRecordsTool = void 0;
4
4
  const generators_1 = require("@constructive-io/graphql-query/generators");
5
- const sdk_1 = require("@constructive-io/sdk");
6
5
  const zod_1 = require("zod");
7
6
  const context_1 = require("../context");
7
+ const graphql_http_1 = require("../graphql-http");
8
8
  const meta_1 = require("../records/meta");
9
9
  function fail(message) {
10
10
  return { content: [{ type: 'text', text: message }], details: { success: false, message } };
11
11
  }
12
+ const UNAVAILABLE = "The app database is unavailable: the backend answered ACCESS_SUSPENDED, so it was deleted or is suspended. Retrying won't help until it is restored.";
13
+ function unavailable(inserted = 0) {
14
+ const message = inserted > 0 ? `${UNAVAILABLE} ${inserted} row(s) were inserted before it stopped.` : `${UNAVAILABLE} No rows were inserted.`;
15
+ return {
16
+ content: [{ type: 'text', text: message }],
17
+ details: { success: false, message, code: 'database-unavailable', ...(inserted > 0 ? { createdCount: inserted } : {}) },
18
+ };
19
+ }
20
+ const isSuspended = (errors) => (0, graphql_http_1.classifyBackendErrors)(errors) === 'suspended';
12
21
  function needsAuth(message) {
13
22
  return {
14
23
  content: [{ type: 'text', text: message }],
@@ -53,14 +62,15 @@ exports.addRecordsTool = {
53
62
  const token = await (0, context_1.resolveDataToken)(resolved.context);
54
63
  if (!token.token)
55
64
  return needsAuth(token.reason ?? 'Sign in to the database to insert records.');
56
- const adapter = new sdk_1.objects.FetchAdapter(dataEndpoint, {
57
- Authorization: `Bearer ${token.token}`,
58
- });
65
+ // Keeps the status and extensions.code that the SDK adapter drops.
66
+ const adapter = (0, graphql_http_1.statusAdapter)(dataEndpoint, { Authorization: `Bearer ${token.token}` });
59
67
  let tables;
60
68
  try {
61
69
  const meta = await adapter.execute(meta_1.META_QUERY);
62
70
  if (!meta.ok) {
63
- return fail(`Failed to read schema from the app database: ${meta.errors.map((e) => e.message).join('; ')}`);
71
+ if (isSuspended(meta.errors))
72
+ return unavailable();
73
+ return fail(`Failed to read schema from the app database: ${(0, graphql_http_1.errorsText)(meta.errors)}`);
64
74
  }
65
75
  tables = (meta.data._meta?.tables ?? []).filter((t) => Boolean(t));
66
76
  }
@@ -91,7 +101,9 @@ exports.addRecordsTool = {
91
101
  try {
92
102
  const result = await adapter.execute(mutation, variables);
93
103
  if (!result.ok) {
94
- errors.push(result.errors.map((e) => e.message).join('; '));
104
+ if (isSuspended(result.errors))
105
+ return unavailable(created.length);
106
+ errors.push((0, graphql_http_1.errorsText)(result.errors));
95
107
  continue;
96
108
  }
97
109
  const payload = result.data[mutationName];
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.addRelationTool = void 0;
4
4
  const harness_1 = require("@agentic-kit/harness");
5
+ const blueprint_construction_1 = require("../blueprint-construction");
5
6
  const context_1 = require("../context");
6
7
  const schema_resolve_1 = require("../schema-resolve");
7
8
  const add_relation_schema_1 = require("./add-relation-schema");
@@ -58,9 +59,7 @@ exports.addRelationTool = {
58
59
  const blueprintId = created.createBlueprint?.blueprint?.id;
59
60
  if (!blueprintId)
60
61
  throw new Error('Failed to create relation blueprint');
61
- await modules.mutation
62
- .constructBlueprint({ input: { blueprintId, schemaId } }, { select: { result: true } })
63
- .unwrap();
62
+ await (0, blueprint_construction_1.buildBlueprint)(modules, { blueprintId, schemaId }, { signal: ctx.signal });
64
63
  }
65
64
  catch (err) {
66
65
  return fail(`Failed to add relation: ${err instanceof Error ? err.message : String(err)}`);
@@ -17,6 +17,8 @@ export type CreateApiKeyDetails = {
17
17
  principalName?: string;
18
18
  scope?: string;
19
19
  needsAuth?: boolean;
20
+ /** The app API answered ACCESS_SUSPENDED: the database is deleted or suspended. */
21
+ code?: 'database-unavailable';
20
22
  };
21
23
  export declare function toEnvVar(keyName: string): string;
22
24
  export declare function isStepUpError(messages: string[]): boolean;
@@ -7,6 +7,7 @@ exports.describeScope = describeScope;
7
7
  const sdk_1 = require("@constructive-io/sdk");
8
8
  const zod_1 = require("zod");
9
9
  const context_1 = require("../context");
10
+ const graphql_http_1 = require("../graphql-http");
10
11
  const host_1 = require("../host");
11
12
  const CreateApiKeyZod = zod_1.z.object({
12
13
  key_name: zod_1.z
@@ -57,19 +58,25 @@ function describeScope(entityIds, readOnly) {
57
58
  parts.push('read-only');
58
59
  return parts.join(', ');
59
60
  }
61
+ // The plain fetch, read at call time. postGraphQL keeps a 403's body, so a
62
+ // deleted or suspended database's ACCESS_SUSPENDED reaches the caller.
60
63
  async function rawGraphql(endpoint, token, query, variables) {
61
- const res = await fetch(endpoint, {
62
- method: 'POST',
63
- headers: {
64
- 'Content-Type': 'application/json',
65
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
66
- },
67
- body: JSON.stringify({ query, variables }),
64
+ const result = await (0, graphql_http_1.postGraphQL)({
65
+ endpoint,
66
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
67
+ query,
68
+ variables,
69
+ fetchFn: (input, init) => fetch(input, init),
68
70
  });
69
- return (await res.json());
71
+ return { data: result.data ?? undefined, errors: result.errors.length > 0 ? result.errors : undefined };
70
72
  }
73
+ const UNAVAILABLE_MESSAGE = 'The app database is unavailable: the backend answered ACCESS_SUSPENDED, so it was deleted or is suspended. No key was minted.';
74
+ const isSuspended = (errors) => Boolean(errors?.length) && (0, graphql_http_1.classifyBackendErrors)(errors) === 'suspended';
75
+ const unavailable = () => fail(UNAVAILABLE_MESSAGE, { code: 'database-unavailable' });
71
76
  async function createPrincipalInputFields(endpoint) {
72
77
  const probe = await rawGraphql(endpoint, undefined, '{ __type(name: "CreatePrincipalInput") { inputFields { name } } }');
78
+ if (isSuspended(probe.errors))
79
+ return 'unavailable';
73
80
  const type = probe.data?.__type;
74
81
  return new Set((type?.inputFields ?? []).map((f) => f.name));
75
82
  }
@@ -107,6 +114,8 @@ exports.createApiKeyTool = {
107
114
  try {
108
115
  if (scoped) {
109
116
  const fields = await createPrincipalInputFields(authEndpoint);
117
+ if (fields === 'unavailable')
118
+ return unavailable();
110
119
  const missing = [
111
120
  ...(params.entity_ids?.length && !fields.has('entityIds') ? ['entityIds'] : []),
112
121
  ...(params.read_only === true && !fields.has('isReadOnly') ? ['isReadOnly'] : []),
@@ -116,8 +125,7 @@ exports.createApiKeyTool = {
116
125
  }
117
126
  }
118
127
  const dbAuth = sdk_1.auth.createClient({
119
- endpoint: authEndpoint,
120
- headers: { Authorization: `Bearer ${token.token}` },
128
+ adapter: (0, graphql_http_1.statusAdapter)(authEndpoint, { Authorization: `Bearer ${token.token}` }),
121
129
  });
122
130
  const principalName = params.principal_name?.trim() || keyName;
123
131
  const existing = await dbAuth.principal
@@ -154,8 +162,10 @@ exports.createApiKeyTool = {
154
162
  ...(params.read_only !== undefined && { isReadOnly: params.read_only }),
155
163
  },
156
164
  });
165
+ if (isSuspended(created.errors))
166
+ return unavailable();
157
167
  if (created.errors?.length) {
158
- return fail(`createPrincipal failed: ${created.errors.map((e) => e.message).join('; ')}`);
168
+ return fail(`createPrincipal failed: ${(0, graphql_http_1.errorsText)(created.errors)}`);
159
169
  }
160
170
  const payload = created.data?.createPrincipal;
161
171
  principalId = payload?.result;
@@ -183,7 +193,9 @@ exports.createApiKeyTool = {
183
193
  minted = await mint();
184
194
  }
185
195
  if (!minted.ok) {
186
- return fail(`createApiKey failed: ${minted.errors.map((e) => e.message).join('; ')}`);
196
+ if (isSuspended(minted.errors))
197
+ return unavailable();
198
+ return fail(`createApiKey failed: ${(0, graphql_http_1.errorsText)(minted.errors)}`);
187
199
  }
188
200
  const record = minted.data?.createApiKey?.result;
189
201
  if (!record?.apiKey || !record.keyId)
@@ -214,6 +226,9 @@ exports.createApiKeyTool = {
214
226
  };
215
227
  }
216
228
  catch (err) {
229
+ // The SDK's unwrap() throws its errors as `errors`.
230
+ if (isSuspended(err?.errors))
231
+ return unavailable();
217
232
  return fail(err instanceof Error ? err.message : 'Failed to create the API key.');
218
233
  }
219
234
  },
@@ -2,8 +2,8 @@ import type { HarnessTool } from '@agentic-kit/harness';
2
2
  import { z } from 'zod';
3
3
  declare const ManageEntityTypesZod: z.ZodObject<{
4
4
  action: z.ZodEnum<{
5
- create: "create";
6
5
  delete: "delete";
6
+ create: "create";
7
7
  list: "list";
8
8
  }>;
9
9
  name: z.ZodOptional<z.ZodString>;
@@ -16,6 +16,12 @@ export type ProvisionBlueprintDetails = {
16
16
  total: number;
17
17
  tables: ProvisionedTable[];
18
18
  error: string | null;
19
+ /**
20
+ * The construction the backend queued, also when the build then failed or
21
+ * the wait gave up: the build carries on, so calling again would build the
22
+ * same tables twice. Null when nothing was queued.
23
+ */
24
+ constructionId: string | null;
19
25
  };
20
26
  export declare const provisionBlueprintTool: HarnessTool<typeof BlueprintZod, ProvisionBlueprintDetails>;
21
27
  export {};
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.provisionBlueprintTool = void 0;
4
4
  const harness_1 = require("@agentic-kit/harness");
5
+ const blueprint_construction_1 = require("../blueprint-construction");
5
6
  const context_1 = require("../context");
6
7
  function formatText(name, details) {
7
8
  if (details.error)
@@ -26,7 +27,13 @@ exports.provisionBlueprintTool = {
26
27
  promptSnippet: 'provision_blueprint: create new tables from scratch in a single call (fields + policies + relations). Call describe_schema first.',
27
28
  parameters: harness_1.BlueprintZod,
28
29
  async execute(params, ctx) {
29
- const empty = { created: 0, total: 0, tables: [], error: null };
30
+ const empty = {
31
+ created: 0,
32
+ total: 0,
33
+ tables: [],
34
+ error: null,
35
+ constructionId: null,
36
+ };
30
37
  const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
31
38
  if (!resolved.context) {
32
39
  return { content: [{ type: 'text', text: resolved.reason }], details: empty };
@@ -57,9 +64,7 @@ exports.provisionBlueprintTool = {
57
64
  const blueprintId = createResult.createBlueprint?.blueprint?.id;
58
65
  if (!blueprintId)
59
66
  throw new Error('Failed to create blueprint');
60
- await modules.mutation
61
- .constructBlueprint({ input: { blueprintId, schemaId } }, { select: { result: true } })
62
- .unwrap();
67
+ const { constructionId } = await (0, blueprint_construction_1.buildBlueprint)(modules, { blueprintId, schemaId }, { signal: ctx.signal });
63
68
  const relations = params.definition.relations ?? [];
64
69
  const details = {
65
70
  created: params.definition.tables.length,
@@ -76,6 +81,7 @@ exports.provisionBlueprintTool = {
76
81
  relationCount: relations.filter((r) => r.source_table === t.table_name).length,
77
82
  })),
78
83
  error: null,
84
+ constructionId,
79
85
  };
80
86
  return { content: [{ type: 'text', text: formatText(params.name, details) }], details };
81
87
  }
@@ -86,6 +92,7 @@ exports.provisionBlueprintTool = {
86
92
  total: params.definition.tables.length,
87
93
  tables: [],
88
94
  error: message,
95
+ constructionId: err instanceof blueprint_construction_1.BlueprintBuildError ? err.constructionId : null,
89
96
  };
90
97
  return { content: [{ type: 'text', text: formatText(params.name, details) }], details };
91
98
  }
@@ -1,17 +1,14 @@
1
1
  import type { HarnessTool } from '@agentic-kit/harness';
2
2
  import { z } from 'zod';
3
+ import { type RunCommand } from '../app-workspace';
4
+ import { type ProvisionDatabaseDetails } from '../provision-database/provision';
5
+ export type { ProvisionDatabaseDetails };
3
6
  declare const ProvisionDatabaseZod: z.ZodObject<{
4
7
  database_name: z.ZodString;
5
8
  reprovision: z.ZodOptional<z.ZodBoolean>;
6
9
  }, z.core.$strip>;
7
- export type ProvisionDatabaseDetails = {
8
- success: boolean;
9
- message: string;
10
- databaseId?: string;
11
- databaseName?: string;
12
- ownerId?: string;
13
- /** True when an existing live binding was kept — nothing changed. */
14
- skipped?: boolean;
15
- };
16
- export declare const provisionDatabaseTool: HarnessTool<typeof ProvisionDatabaseZod, ProvisionDatabaseDetails>;
17
- export {};
10
+ export declare function createProvisionDatabaseTool(exec?: RunCommand): HarnessTool<typeof ProvisionDatabaseZod, ProvisionDatabaseDetails>;
11
+ export declare const provisionDatabaseTool: HarnessTool<z.ZodObject<{
12
+ database_name: z.ZodString;
13
+ reprovision: z.ZodOptional<z.ZodBoolean>;
14
+ }, z.core.$strip>, ProvisionDatabaseDetails>;