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