@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.
- package/README.md +40 -1
- package/app-workspace.d.ts +29 -2
- package/app-workspace.js +99 -8
- package/blueprint-construction.d.ts +60 -0
- package/blueprint-construction.js +161 -0
- package/context.d.ts +5 -1
- package/context.js +30 -5
- package/db-probe.d.ts +25 -17
- package/db-probe.js +97 -37
- package/esm/app-workspace.d.ts +29 -2
- package/esm/app-workspace.js +96 -8
- package/esm/blueprint-construction.d.ts +60 -0
- package/esm/blueprint-construction.js +154 -0
- package/esm/context.d.ts +5 -1
- package/esm/context.js +31 -6
- package/esm/db-probe.d.ts +25 -17
- package/esm/db-probe.js +96 -37
- package/esm/graphql-http.d.ts +35 -0
- package/esm/graphql-http.js +134 -0
- package/esm/host.d.ts +46 -0
- package/esm/host.js +9 -0
- package/esm/index.d.ts +11 -2
- package/esm/index.js +6 -0
- package/esm/provision-database/env-file.d.ts +23 -2
- package/esm/provision-database/env-file.js +86 -27
- package/esm/provision-database/pending-ticket.d.ts +33 -0
- package/esm/provision-database/pending-ticket.js +63 -0
- package/esm/provision-database/provision.d.ts +71 -0
- package/esm/provision-database/provision.js +557 -0
- package/esm/provision-database/request-database.d.ts +55 -3
- package/esm/provision-database/request-database.js +118 -17
- package/esm/schema-model/normalize.d.ts +21 -0
- package/esm/schema-model/normalize.js +244 -0
- package/esm/schema-model.d.ts +110 -0
- package/esm/schema-model.js +303 -0
- package/esm/tools/add-policies.d.ts +1 -1
- package/esm/tools/add-records.d.ts +1 -0
- package/esm/tools/add-records.js +18 -6
- package/esm/tools/add-relation.js +2 -3
- package/esm/tools/create-api-key.d.ts +2 -0
- package/esm/tools/create-api-key.js +27 -12
- package/esm/tools/manage-entity-types.d.ts +1 -1
- package/esm/tools/provision-blueprint.d.ts +6 -0
- package/esm/tools/provision-blueprint.js +11 -4
- package/esm/tools/provision-database.d.ts +8 -11
- package/esm/tools/provision-database.js +23 -243
- package/esm/tools/run-codegen.d.ts +19 -2
- package/esm/tools/run-codegen.js +184 -97
- package/esm/tools/templates.js +3 -15
- package/graphql-http.d.ts +35 -0
- package/graphql-http.js +140 -0
- package/host.d.ts +46 -0
- package/host.js +10 -0
- package/index.d.ts +11 -2
- package/index.js +26 -1
- package/package.json +4 -4
- package/provision-database/env-file.d.ts +23 -2
- package/provision-database/env-file.js +89 -28
- package/provision-database/pending-ticket.d.ts +33 -0
- package/provision-database/pending-ticket.js +72 -0
- package/provision-database/provision.d.ts +71 -0
- package/provision-database/provision.js +565 -0
- package/provision-database/request-database.d.ts +55 -3
- package/provision-database/request-database.js +124 -18
- package/schema-model/normalize.d.ts +21 -0
- package/schema-model/normalize.js +250 -0
- package/schema-model.d.ts +110 -0
- package/schema-model.js +309 -0
- package/tools/add-policies.d.ts +1 -1
- package/tools/add-records.d.ts +1 -0
- package/tools/add-records.js +18 -6
- package/tools/add-relation.js +2 -3
- package/tools/create-api-key.d.ts +2 -0
- package/tools/create-api-key.js +27 -12
- package/tools/manage-entity-types.d.ts +1 -1
- package/tools/provision-blueprint.d.ts +6 -0
- package/tools/provision-blueprint.js +11 -4
- package/tools/provision-database.d.ts +8 -11
- package/tools/provision-database.js +23 -245
- package/tools/run-codegen.d.ts +19 -2
- package/tools/run-codegen.js +184 -96
- package/tools/templates.js +3 -15
package/schema-model.js
ADDED
|
@@ -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
|
+
}
|
package/tools/add-policies.d.ts
CHANGED
|
@@ -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>;
|
package/tools/add-records.d.ts
CHANGED
package/tools/add-records.js
CHANGED
|
@@ -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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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];
|
package/tools/add-relation.js
CHANGED
|
@@ -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.
|
|
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;
|
package/tools/create-api-key.js
CHANGED
|
@@ -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
|
|
62
|
-
|
|
63
|
-
headers: {
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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 = {
|
|
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.
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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>;
|