@agentic-kit/db-tools 0.8.2 → 0.9.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.
- package/context.js +18 -18
- package/esm/context.js +18 -18
- package/esm/tools/provision-database.d.ts +0 -1
- package/esm/tools/provision-database.js +2 -8
- package/esm/tools/templates.d.ts +1 -1
- package/package.json +5 -5
- package/tools/provision-database.d.ts +0 -1
- package/tools/provision-database.js +2 -8
- package/tools/templates.d.ts +1 -1
- package/esm/provision-database/pg-fixups.d.ts +0 -16
- package/esm/provision-database/pg-fixups.js +0 -132
- package/provision-database/pg-fixups.d.ts +0 -16
- package/provision-database/pg-fixups.js +0 -137
package/context.js
CHANGED
|
@@ -155,14 +155,11 @@ async function resolveProjectContext(input, options = {}) {
|
|
|
155
155
|
const dataEndpoint = databaseName
|
|
156
156
|
? deriveSubdomainEndpoint(apiEndpoint, `api-${databaseName}`)
|
|
157
157
|
: '';
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
return {
|
|
161
|
-
context: null,
|
|
162
|
-
reason: 'Could not resolve a schema (app_public/public) for this database. The database may not be fully provisioned yet.',
|
|
163
|
-
code: 'schema-unresolved',
|
|
164
|
-
};
|
|
158
|
+
const schema = await resolveSchemaId(apiClient, databaseId);
|
|
159
|
+
if ('reason' in schema) {
|
|
160
|
+
return { context: null, reason: schema.reason, code: 'schema-unresolved' };
|
|
165
161
|
}
|
|
162
|
+
const schemaId = schema.id;
|
|
166
163
|
return {
|
|
167
164
|
context: {
|
|
168
165
|
api: apiClient,
|
|
@@ -262,21 +259,24 @@ async function resolveOrgName(ownerId) {
|
|
|
262
259
|
return undefined;
|
|
263
260
|
}
|
|
264
261
|
}
|
|
262
|
+
// `name` is the logical schema name, identical on every backend; the physical
|
|
263
|
+
// (possibly hashed) name is `schemaName`.
|
|
265
264
|
async function resolveSchemaId(apiClient, databaseId) {
|
|
266
265
|
const result = await apiClient.schema
|
|
267
266
|
.findMany({
|
|
268
267
|
select: { id: true, name: true },
|
|
269
|
-
where: { databaseId: { equalTo: databaseId } },
|
|
268
|
+
where: { databaseId: { equalTo: databaseId }, name: { equalTo: 'app_public' } },
|
|
270
269
|
})
|
|
271
270
|
.execute();
|
|
272
|
-
if (!result.ok)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
271
|
+
if (!result.ok) {
|
|
272
|
+
const detail = result.errors?.[0]?.message ?? 'unknown error';
|
|
273
|
+
return { reason: `Could not query the app_public schema for database ${databaseId}: ${detail}` };
|
|
274
|
+
}
|
|
275
|
+
const appPublic = (result.data.schemas?.nodes ?? []).find((s) => s?.name === 'app_public');
|
|
276
|
+
if (!appPublic?.id) {
|
|
277
|
+
return {
|
|
278
|
+
reason: `Database ${databaseId} has no app_public schema. The database may not be fully provisioned yet.`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { id: appPublic.id };
|
|
282
282
|
}
|
package/esm/context.js
CHANGED
|
@@ -143,14 +143,11 @@ export async function resolveProjectContext(input, options = {}) {
|
|
|
143
143
|
const dataEndpoint = databaseName
|
|
144
144
|
? deriveSubdomainEndpoint(apiEndpoint, `api-${databaseName}`)
|
|
145
145
|
: '';
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
return {
|
|
149
|
-
context: null,
|
|
150
|
-
reason: 'Could not resolve a schema (app_public/public) for this database. The database may not be fully provisioned yet.',
|
|
151
|
-
code: 'schema-unresolved',
|
|
152
|
-
};
|
|
146
|
+
const schema = await resolveSchemaId(apiClient, databaseId);
|
|
147
|
+
if ('reason' in schema) {
|
|
148
|
+
return { context: null, reason: schema.reason, code: 'schema-unresolved' };
|
|
153
149
|
}
|
|
150
|
+
const schemaId = schema.id;
|
|
154
151
|
return {
|
|
155
152
|
context: {
|
|
156
153
|
api: apiClient,
|
|
@@ -250,21 +247,24 @@ export async function resolveOrgName(ownerId) {
|
|
|
250
247
|
return undefined;
|
|
251
248
|
}
|
|
252
249
|
}
|
|
250
|
+
// `name` is the logical schema name, identical on every backend; the physical
|
|
251
|
+
// (possibly hashed) name is `schemaName`.
|
|
253
252
|
async function resolveSchemaId(apiClient, databaseId) {
|
|
254
253
|
const result = await apiClient.schema
|
|
255
254
|
.findMany({
|
|
256
255
|
select: { id: true, name: true },
|
|
257
|
-
where: { databaseId: { equalTo: databaseId } },
|
|
256
|
+
where: { databaseId: { equalTo: databaseId }, name: { equalTo: 'app_public' } },
|
|
258
257
|
})
|
|
259
258
|
.execute();
|
|
260
|
-
if (!result.ok)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
259
|
+
if (!result.ok) {
|
|
260
|
+
const detail = result.errors?.[0]?.message ?? 'unknown error';
|
|
261
|
+
return { reason: `Could not query the app_public schema for database ${databaseId}: ${detail}` };
|
|
262
|
+
}
|
|
263
|
+
const appPublic = (result.data.schemas?.nodes ?? []).find((s) => s?.name === 'app_public');
|
|
264
|
+
if (!appPublic?.id) {
|
|
265
|
+
return {
|
|
266
|
+
reason: `Database ${databaseId} has no app_public schema. The database may not be fully provisioned yet.`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { id: appPublic.id };
|
|
270
270
|
}
|
|
@@ -8,7 +8,6 @@ import { getHost } from '../host';
|
|
|
8
8
|
import { selectProvisionCredential } from '../provision-database/credential';
|
|
9
9
|
import { archiveBindingKeys, ARCHIVED_BINDING_KEYS, mergeEnv, provisionEnvVars, } from '../provision-database/env-file';
|
|
10
10
|
import { loadProvisionManifest } from '../provision-database/manifest';
|
|
11
|
-
import { applySqlFixups } from '../provision-database/pg-fixups';
|
|
12
11
|
import { selectProvisionRequest } from '../provision-database/preset-match';
|
|
13
12
|
import { requestDatabaseProvision } from '../provision-database/request-database';
|
|
14
13
|
import { resolveProvisionModules } from '../provision-database/resolve';
|
|
@@ -60,7 +59,7 @@ function parseEnvKeys(source) {
|
|
|
60
59
|
export const provisionDatabaseTool = {
|
|
61
60
|
name: 'provision_database',
|
|
62
61
|
label: 'Provision database',
|
|
63
|
-
description: 'Bootstrap a new Constructive database for the project under your account: provision the standard module set
|
|
62
|
+
description: 'Bootstrap a new Constructive database for the project under your account: provision the standard module set and write credentials (DATABASE_ID, ACCESS_TOKEN, etc.) to the project .env. Run this ONCE before any schema/record tools. If the project is already bound to a live database under the signed-in account it skips; if the bound database no longer exists on this backend (refreshed/deleted) or belongs to a different account, pass reprovision: true to mint a fresh one (old keys archived in .env, old database kept; rebuild the schema afterwards).',
|
|
64
63
|
promptSnippet: 'provision_database: one-time bootstrap of the project database (owner + modules + .env). Run before describe_schema/provision_blueprint. Skips when the existing binding is live under the signed-in account; reprovision: true replaces a dead or foreign-account binding (archives old keys, never deletes the old db). Gated.',
|
|
65
64
|
parameters: ProvisionDatabaseZod,
|
|
66
65
|
async execute(params, ctx) {
|
|
@@ -191,7 +190,6 @@ export const provisionDatabaseTool = {
|
|
|
191
190
|
catch (err) {
|
|
192
191
|
return fail(`Cannot resolve provision modules: ${err instanceof Error ? err.message : String(err)}`);
|
|
193
192
|
}
|
|
194
|
-
const physicalDb = process.env.CONSTRUCTIVE_DB || 'constructive';
|
|
195
193
|
// Provision on the API endpoint: requestDatabase claims a warm-pool
|
|
196
194
|
// database when the resolved module set matches a cataloged preset
|
|
197
195
|
// (near-instant) and cold-provisions asynchronously otherwise; the ticket
|
|
@@ -219,9 +217,6 @@ export const provisionDatabaseTool = {
|
|
|
219
217
|
: '';
|
|
220
218
|
return fail(`Database provisioning failed: ${detail}.${nameTakenHint}`);
|
|
221
219
|
}
|
|
222
|
-
// Enable membership defaults + naming settings at the SQL level. Best-effort:
|
|
223
|
-
// provisioning already succeeded, so a fixup failure is a warning, not an error.
|
|
224
|
-
const fixup = await applySqlFixups({ databaseName, physicalDb });
|
|
225
220
|
// Persist the binding to the project .env (upsert, preserving other keys).
|
|
226
221
|
const merged = mergeEnv(existing, provisionEnvVars({
|
|
227
222
|
databaseId,
|
|
@@ -244,7 +239,7 @@ export const provisionDatabaseTool = {
|
|
|
244
239
|
const reprovisionNote = hasBinding && params.reprovision
|
|
245
240
|
? ' Previous binding archived in .env (old database kept); rebuild the schema with provision_blueprint + run_codegen.'
|
|
246
241
|
: '';
|
|
247
|
-
const message = `Provisioned database "${databaseName}" (ID: ${databaseId}). Credentials written to .env
|
|
242
|
+
const message = `Provisioned database "${databaseName}" (ID: ${databaseId}). Credentials written to .env.${prewarmNote}${reprovisionNote}`;
|
|
248
243
|
return {
|
|
249
244
|
content: [{ type: 'text', text: message }],
|
|
250
245
|
details: {
|
|
@@ -253,7 +248,6 @@ export const provisionDatabaseTool = {
|
|
|
253
248
|
databaseId,
|
|
254
249
|
databaseName,
|
|
255
250
|
ownerId,
|
|
256
|
-
fixupNote: fixup.note,
|
|
257
251
|
},
|
|
258
252
|
};
|
|
259
253
|
},
|
package/esm/tools/templates.d.ts
CHANGED
|
@@ -58,8 +58,8 @@ declare const UpdateTemplateZod: z.ZodObject<{
|
|
|
58
58
|
categories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
59
59
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
60
60
|
visibility: z.ZodOptional<z.ZodEnum<{
|
|
61
|
-
public: "public";
|
|
62
61
|
private: "private";
|
|
62
|
+
public: "public";
|
|
63
63
|
}>>;
|
|
64
64
|
}, z.core.$strip>;
|
|
65
65
|
}, z.core.$strip>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentic-kit/db-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "harness-neutral Constructive database tools — provisioning, schema, policies, records, templates and codegen as HarnessTools any adapter can bind",
|
|
6
6
|
"main": "index.js",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"12factor-env": "^1.33.0",
|
|
33
33
|
"@agentic-kit/harness": "^0.16.0",
|
|
34
|
-
"@constructive-io/graphql-query": "^4.20.
|
|
35
|
-
"@constructive-io/sdk": "^1.19.
|
|
36
|
-
"node-type-registry": "^1.19.
|
|
34
|
+
"@constructive-io/graphql-query": "^4.20.4",
|
|
35
|
+
"@constructive-io/sdk": "^1.19.4",
|
|
36
|
+
"node-type-registry": "^1.19.1",
|
|
37
37
|
"zod": "^4.4.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"database",
|
|
46
46
|
"constructive"
|
|
47
47
|
],
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "ee458dba426522b0d06e315f6ef010938ed71e0e"
|
|
49
49
|
}
|
|
@@ -14,7 +14,6 @@ const host_1 = require("../host");
|
|
|
14
14
|
const credential_1 = require("../provision-database/credential");
|
|
15
15
|
const env_file_1 = require("../provision-database/env-file");
|
|
16
16
|
const manifest_1 = require("../provision-database/manifest");
|
|
17
|
-
const pg_fixups_1 = require("../provision-database/pg-fixups");
|
|
18
17
|
const preset_match_1 = require("../provision-database/preset-match");
|
|
19
18
|
const request_database_1 = require("../provision-database/request-database");
|
|
20
19
|
const resolve_1 = require("../provision-database/resolve");
|
|
@@ -66,7 +65,7 @@ function parseEnvKeys(source) {
|
|
|
66
65
|
exports.provisionDatabaseTool = {
|
|
67
66
|
name: 'provision_database',
|
|
68
67
|
label: 'Provision database',
|
|
69
|
-
description: 'Bootstrap a new Constructive database for the project under your account: provision the standard module set
|
|
68
|
+
description: 'Bootstrap a new Constructive database for the project under your account: provision the standard module set and write credentials (DATABASE_ID, ACCESS_TOKEN, etc.) to the project .env. Run this ONCE before any schema/record tools. If the project is already bound to a live database under the signed-in account it skips; if the bound database no longer exists on this backend (refreshed/deleted) or belongs to a different account, pass reprovision: true to mint a fresh one (old keys archived in .env, old database kept; rebuild the schema afterwards).',
|
|
70
69
|
promptSnippet: 'provision_database: one-time bootstrap of the project database (owner + modules + .env). Run before describe_schema/provision_blueprint. Skips when the existing binding is live under the signed-in account; reprovision: true replaces a dead or foreign-account binding (archives old keys, never deletes the old db). Gated.',
|
|
71
70
|
parameters: ProvisionDatabaseZod,
|
|
72
71
|
async execute(params, ctx) {
|
|
@@ -197,7 +196,6 @@ exports.provisionDatabaseTool = {
|
|
|
197
196
|
catch (err) {
|
|
198
197
|
return fail(`Cannot resolve provision modules: ${err instanceof Error ? err.message : String(err)}`);
|
|
199
198
|
}
|
|
200
|
-
const physicalDb = process.env.CONSTRUCTIVE_DB || 'constructive';
|
|
201
199
|
// Provision on the API endpoint: requestDatabase claims a warm-pool
|
|
202
200
|
// database when the resolved module set matches a cataloged preset
|
|
203
201
|
// (near-instant) and cold-provisions asynchronously otherwise; the ticket
|
|
@@ -225,9 +223,6 @@ exports.provisionDatabaseTool = {
|
|
|
225
223
|
: '';
|
|
226
224
|
return fail(`Database provisioning failed: ${detail}.${nameTakenHint}`);
|
|
227
225
|
}
|
|
228
|
-
// Enable membership defaults + naming settings at the SQL level. Best-effort:
|
|
229
|
-
// provisioning already succeeded, so a fixup failure is a warning, not an error.
|
|
230
|
-
const fixup = await (0, pg_fixups_1.applySqlFixups)({ databaseName, physicalDb });
|
|
231
226
|
// Persist the binding to the project .env (upsert, preserving other keys).
|
|
232
227
|
const merged = (0, env_file_1.mergeEnv)(existing, (0, env_file_1.provisionEnvVars)({
|
|
233
228
|
databaseId,
|
|
@@ -250,7 +245,7 @@ exports.provisionDatabaseTool = {
|
|
|
250
245
|
const reprovisionNote = hasBinding && params.reprovision
|
|
251
246
|
? ' Previous binding archived in .env (old database kept); rebuild the schema with provision_blueprint + run_codegen.'
|
|
252
247
|
: '';
|
|
253
|
-
const message = `Provisioned database "${databaseName}" (ID: ${databaseId}). Credentials written to .env
|
|
248
|
+
const message = `Provisioned database "${databaseName}" (ID: ${databaseId}). Credentials written to .env.${prewarmNote}${reprovisionNote}`;
|
|
254
249
|
return {
|
|
255
250
|
content: [{ type: 'text', text: message }],
|
|
256
251
|
details: {
|
|
@@ -259,7 +254,6 @@ exports.provisionDatabaseTool = {
|
|
|
259
254
|
databaseId,
|
|
260
255
|
databaseName,
|
|
261
256
|
ownerId,
|
|
262
|
-
fixupNote: fixup.note,
|
|
263
257
|
},
|
|
264
258
|
};
|
|
265
259
|
},
|
package/tools/templates.d.ts
CHANGED
|
@@ -58,8 +58,8 @@ declare const UpdateTemplateZod: z.ZodObject<{
|
|
|
58
58
|
categories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
59
59
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
60
60
|
visibility: z.ZodOptional<z.ZodEnum<{
|
|
61
|
-
public: "public";
|
|
62
61
|
private: "private";
|
|
62
|
+
public: "public";
|
|
63
63
|
}>>;
|
|
64
64
|
}, z.core.$strip>;
|
|
65
65
|
}, z.core.$strip>;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export type FixupResult = {
|
|
2
|
-
applied: boolean;
|
|
3
|
-
note: string;
|
|
4
|
-
};
|
|
5
|
-
export type PgEnv = {
|
|
6
|
-
host?: string;
|
|
7
|
-
port?: string;
|
|
8
|
-
user?: string;
|
|
9
|
-
password?: string;
|
|
10
|
-
};
|
|
11
|
-
export declare function parsePgpmEnv(stdout: string): PgEnv;
|
|
12
|
-
export declare function resolvePgEnv(): Promise<PgEnv | null>;
|
|
13
|
-
export declare function applySqlFixups(args: {
|
|
14
|
-
databaseName: string;
|
|
15
|
-
physicalDb: string;
|
|
16
|
-
}): Promise<FixupResult>;
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
// Post-provision SQL fixups for a freshly provisioned database.
|
|
2
|
-
//
|
|
3
|
-
// Two things must happen at the Postgres level that the provisioning GraphQL API
|
|
4
|
-
// does not do for us:
|
|
5
|
-
//
|
|
6
|
-
// 1. Naming settings — `constructive.simple_schema_names` and
|
|
7
|
-
// `schema_use_underscores` make later `provision_blueprint` calls create an
|
|
8
|
-
// `app_public` schema (which resolveSchemaId looks for) instead of a
|
|
9
|
-
// hyphenated `<db>-<hex>-app-public` one. These ALTER DATABASE settings
|
|
10
|
-
// PERSIST, so we set them once here, before any blueprint construction.
|
|
11
|
-
//
|
|
12
|
-
// 2. Membership defaults — new sign-ups otherwise land with
|
|
13
|
-
// is_approved=false/is_verified=false, the AuthzEntityMembership policy then
|
|
14
|
-
// denies every insert/select, and CRUD rows silently never persist
|
|
15
|
-
// (BLUEPRINT-PENDING-001). We flip the defaults (future sign-ups) AND any
|
|
16
|
-
// memberships already created during provisioning.
|
|
17
|
-
//
|
|
18
|
-
// All SQL targets the PHYSICAL control-plane database (`constructive`), NOT the
|
|
19
|
-
// app's database name — that name is only a schema prefix (MEMBERSHIP-DB-001).
|
|
20
|
-
//
|
|
21
|
-
// Connection creds come from the ambient PG* env vars, falling back to sourcing
|
|
22
|
-
// `pgpm env` (the same mechanism the old `eval "$(pgpm env)"` shell step used).
|
|
23
|
-
// If neither yields a connection, provisioning still succeeded — we return a
|
|
24
|
-
// note so the tool can warn the user that sign-in/CRUD may need a manual fixup.
|
|
25
|
-
import { execFile } from 'node:child_process';
|
|
26
|
-
import { promisify } from 'node:util';
|
|
27
|
-
const execFileAsync = promisify(execFile);
|
|
28
|
-
// Parse `pgpm env` output. It prints shell assignments (optionally `export`-
|
|
29
|
-
// prefixed), one per line: `export PGHOST=localhost`. Quotes are stripped.
|
|
30
|
-
export function parsePgpmEnv(stdout) {
|
|
31
|
-
const env = {};
|
|
32
|
-
for (const rawLine of stdout.split('\n')) {
|
|
33
|
-
const line = rawLine.trim().replace(/^export\s+/, '');
|
|
34
|
-
const eq = line.indexOf('=');
|
|
35
|
-
if (eq === -1)
|
|
36
|
-
continue;
|
|
37
|
-
const key = line.slice(0, eq).trim();
|
|
38
|
-
let value = line.slice(eq + 1).trim();
|
|
39
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
40
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
41
|
-
value = value.slice(1, -1);
|
|
42
|
-
}
|
|
43
|
-
env[key] = value;
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
host: env.PGHOST,
|
|
47
|
-
port: env.PGPORT,
|
|
48
|
-
user: env.PGUSER,
|
|
49
|
-
password: env.PGPASSWORD,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
export async function resolvePgEnv() {
|
|
53
|
-
if (process.env.PGHOST) {
|
|
54
|
-
return {
|
|
55
|
-
host: process.env.PGHOST,
|
|
56
|
-
port: process.env.PGPORT,
|
|
57
|
-
user: process.env.PGUSER,
|
|
58
|
-
password: process.env.PGPASSWORD,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const { stdout } = await execFileAsync('pgpm', ['env'], { timeout: 15_000 });
|
|
63
|
-
const parsed = parsePgpmEnv(stdout);
|
|
64
|
-
return parsed.host ? parsed : null;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// Find the memberships schema for this app. Schema names use hyphens locally
|
|
71
|
-
// (`<db>-<hex>-memberships-public`) but may use underscores when the naming
|
|
72
|
-
// settings are active — match both, scoped to this app's name prefix.
|
|
73
|
-
const MEMBERSHIPS_SCHEMA_QUERY = `
|
|
74
|
-
SELECT schema_name FROM information_schema.schemata
|
|
75
|
-
WHERE (schema_name LIKE '%memberships-public' OR schema_name LIKE '%memberships_public')
|
|
76
|
-
AND (schema_name LIKE $1 OR schema_name LIKE $2)
|
|
77
|
-
ORDER BY schema_name DESC LIMIT 1`;
|
|
78
|
-
export async function applySqlFixups(args) {
|
|
79
|
-
const pgEnv = await resolvePgEnv();
|
|
80
|
-
if (!pgEnv) {
|
|
81
|
-
return {
|
|
82
|
-
applied: false,
|
|
83
|
-
note: 'Could not reach Postgres (no PGHOST and `pgpm env` unavailable). Database provisioned, but membership defaults were not enabled — sign-in/CRUD may fail until the constructive DB is fixed up manually.',
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
// pg is dynamically imported so the main bundle never loads it unless a
|
|
87
|
-
// provision actually runs with Postgres access. The import lives inside the
|
|
88
|
-
// try: fixups are best-effort, so even a broken pg module must degrade to a
|
|
89
|
-
// note instead of failing the tool after the database is already provisioned.
|
|
90
|
-
let pool;
|
|
91
|
-
try {
|
|
92
|
-
const { Pool } = await import('pg');
|
|
93
|
-
pool = new Pool({
|
|
94
|
-
host: pgEnv.host,
|
|
95
|
-
port: pgEnv.port ? Number(pgEnv.port) : undefined,
|
|
96
|
-
user: pgEnv.user,
|
|
97
|
-
password: pgEnv.password,
|
|
98
|
-
database: args.physicalDb,
|
|
99
|
-
});
|
|
100
|
-
const db = args.physicalDb;
|
|
101
|
-
// Persistent naming settings — so future provision_blueprint calls land an
|
|
102
|
-
// `app_public` schema resolveSchemaId can find.
|
|
103
|
-
await pool.query(`ALTER DATABASE "${db}" SET constructive.simple_schema_names = 'true'`);
|
|
104
|
-
await pool.query(`ALTER DATABASE "${db}" SET constructive.schema_use_underscores = 'true'`);
|
|
105
|
-
const schemaResult = await pool.query(MEMBERSHIPS_SCHEMA_QUERY, [
|
|
106
|
-
`${args.databaseName}-%`,
|
|
107
|
-
`${args.databaseName}\\_%`,
|
|
108
|
-
]);
|
|
109
|
-
if (schemaResult.rows.length === 0) {
|
|
110
|
-
return {
|
|
111
|
-
applied: true,
|
|
112
|
-
note: `Naming settings applied, but no memberships schema was found for "${args.databaseName}" — sign-in may need a manual membership fixup.`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
const membershipsSchema = schemaResult.rows[0].schema_name;
|
|
116
|
-
await pool.query(`UPDATE "${membershipsSchema}".app_membership_defaults SET is_approved = TRUE, is_verified = TRUE`);
|
|
117
|
-
await pool.query(`UPDATE "${membershipsSchema}".app_memberships
|
|
118
|
-
SET is_approved = TRUE, is_verified = TRUE
|
|
119
|
-
WHERE is_approved = FALSE OR is_verified = FALSE`);
|
|
120
|
-
return { applied: true, note: `Membership defaults enabled (schema: ${membershipsSchema}).` };
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
-
return {
|
|
125
|
-
applied: false,
|
|
126
|
-
note: `Database provisioned, but SQL fixups failed (${message}). Sign-in/CRUD may fail until membership defaults are enabled manually.`,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
await pool?.end().catch(() => { });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export type FixupResult = {
|
|
2
|
-
applied: boolean;
|
|
3
|
-
note: string;
|
|
4
|
-
};
|
|
5
|
-
export type PgEnv = {
|
|
6
|
-
host?: string;
|
|
7
|
-
port?: string;
|
|
8
|
-
user?: string;
|
|
9
|
-
password?: string;
|
|
10
|
-
};
|
|
11
|
-
export declare function parsePgpmEnv(stdout: string): PgEnv;
|
|
12
|
-
export declare function resolvePgEnv(): Promise<PgEnv | null>;
|
|
13
|
-
export declare function applySqlFixups(args: {
|
|
14
|
-
databaseName: string;
|
|
15
|
-
physicalDb: string;
|
|
16
|
-
}): Promise<FixupResult>;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Post-provision SQL fixups for a freshly provisioned database.
|
|
3
|
-
//
|
|
4
|
-
// Two things must happen at the Postgres level that the provisioning GraphQL API
|
|
5
|
-
// does not do for us:
|
|
6
|
-
//
|
|
7
|
-
// 1. Naming settings — `constructive.simple_schema_names` and
|
|
8
|
-
// `schema_use_underscores` make later `provision_blueprint` calls create an
|
|
9
|
-
// `app_public` schema (which resolveSchemaId looks for) instead of a
|
|
10
|
-
// hyphenated `<db>-<hex>-app-public` one. These ALTER DATABASE settings
|
|
11
|
-
// PERSIST, so we set them once here, before any blueprint construction.
|
|
12
|
-
//
|
|
13
|
-
// 2. Membership defaults — new sign-ups otherwise land with
|
|
14
|
-
// is_approved=false/is_verified=false, the AuthzEntityMembership policy then
|
|
15
|
-
// denies every insert/select, and CRUD rows silently never persist
|
|
16
|
-
// (BLUEPRINT-PENDING-001). We flip the defaults (future sign-ups) AND any
|
|
17
|
-
// memberships already created during provisioning.
|
|
18
|
-
//
|
|
19
|
-
// All SQL targets the PHYSICAL control-plane database (`constructive`), NOT the
|
|
20
|
-
// app's database name — that name is only a schema prefix (MEMBERSHIP-DB-001).
|
|
21
|
-
//
|
|
22
|
-
// Connection creds come from the ambient PG* env vars, falling back to sourcing
|
|
23
|
-
// `pgpm env` (the same mechanism the old `eval "$(pgpm env)"` shell step used).
|
|
24
|
-
// If neither yields a connection, provisioning still succeeded — we return a
|
|
25
|
-
// note so the tool can warn the user that sign-in/CRUD may need a manual fixup.
|
|
26
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
-
exports.parsePgpmEnv = parsePgpmEnv;
|
|
28
|
-
exports.resolvePgEnv = resolvePgEnv;
|
|
29
|
-
exports.applySqlFixups = applySqlFixups;
|
|
30
|
-
const node_child_process_1 = require("node:child_process");
|
|
31
|
-
const node_util_1 = require("node:util");
|
|
32
|
-
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
33
|
-
// Parse `pgpm env` output. It prints shell assignments (optionally `export`-
|
|
34
|
-
// prefixed), one per line: `export PGHOST=localhost`. Quotes are stripped.
|
|
35
|
-
function parsePgpmEnv(stdout) {
|
|
36
|
-
const env = {};
|
|
37
|
-
for (const rawLine of stdout.split('\n')) {
|
|
38
|
-
const line = rawLine.trim().replace(/^export\s+/, '');
|
|
39
|
-
const eq = line.indexOf('=');
|
|
40
|
-
if (eq === -1)
|
|
41
|
-
continue;
|
|
42
|
-
const key = line.slice(0, eq).trim();
|
|
43
|
-
let value = line.slice(eq + 1).trim();
|
|
44
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
45
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
46
|
-
value = value.slice(1, -1);
|
|
47
|
-
}
|
|
48
|
-
env[key] = value;
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
host: env.PGHOST,
|
|
52
|
-
port: env.PGPORT,
|
|
53
|
-
user: env.PGUSER,
|
|
54
|
-
password: env.PGPASSWORD,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
async function resolvePgEnv() {
|
|
58
|
-
if (process.env.PGHOST) {
|
|
59
|
-
return {
|
|
60
|
-
host: process.env.PGHOST,
|
|
61
|
-
port: process.env.PGPORT,
|
|
62
|
-
user: process.env.PGUSER,
|
|
63
|
-
password: process.env.PGPASSWORD,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
const { stdout } = await execFileAsync('pgpm', ['env'], { timeout: 15_000 });
|
|
68
|
-
const parsed = parsePgpmEnv(stdout);
|
|
69
|
-
return parsed.host ? parsed : null;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Find the memberships schema for this app. Schema names use hyphens locally
|
|
76
|
-
// (`<db>-<hex>-memberships-public`) but may use underscores when the naming
|
|
77
|
-
// settings are active — match both, scoped to this app's name prefix.
|
|
78
|
-
const MEMBERSHIPS_SCHEMA_QUERY = `
|
|
79
|
-
SELECT schema_name FROM information_schema.schemata
|
|
80
|
-
WHERE (schema_name LIKE '%memberships-public' OR schema_name LIKE '%memberships_public')
|
|
81
|
-
AND (schema_name LIKE $1 OR schema_name LIKE $2)
|
|
82
|
-
ORDER BY schema_name DESC LIMIT 1`;
|
|
83
|
-
async function applySqlFixups(args) {
|
|
84
|
-
const pgEnv = await resolvePgEnv();
|
|
85
|
-
if (!pgEnv) {
|
|
86
|
-
return {
|
|
87
|
-
applied: false,
|
|
88
|
-
note: 'Could not reach Postgres (no PGHOST and `pgpm env` unavailable). Database provisioned, but membership defaults were not enabled — sign-in/CRUD may fail until the constructive DB is fixed up manually.',
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
// pg is dynamically imported so the main bundle never loads it unless a
|
|
92
|
-
// provision actually runs with Postgres access. The import lives inside the
|
|
93
|
-
// try: fixups are best-effort, so even a broken pg module must degrade to a
|
|
94
|
-
// note instead of failing the tool after the database is already provisioned.
|
|
95
|
-
let pool;
|
|
96
|
-
try {
|
|
97
|
-
const { Pool } = await import('pg');
|
|
98
|
-
pool = new Pool({
|
|
99
|
-
host: pgEnv.host,
|
|
100
|
-
port: pgEnv.port ? Number(pgEnv.port) : undefined,
|
|
101
|
-
user: pgEnv.user,
|
|
102
|
-
password: pgEnv.password,
|
|
103
|
-
database: args.physicalDb,
|
|
104
|
-
});
|
|
105
|
-
const db = args.physicalDb;
|
|
106
|
-
// Persistent naming settings — so future provision_blueprint calls land an
|
|
107
|
-
// `app_public` schema resolveSchemaId can find.
|
|
108
|
-
await pool.query(`ALTER DATABASE "${db}" SET constructive.simple_schema_names = 'true'`);
|
|
109
|
-
await pool.query(`ALTER DATABASE "${db}" SET constructive.schema_use_underscores = 'true'`);
|
|
110
|
-
const schemaResult = await pool.query(MEMBERSHIPS_SCHEMA_QUERY, [
|
|
111
|
-
`${args.databaseName}-%`,
|
|
112
|
-
`${args.databaseName}\\_%`,
|
|
113
|
-
]);
|
|
114
|
-
if (schemaResult.rows.length === 0) {
|
|
115
|
-
return {
|
|
116
|
-
applied: true,
|
|
117
|
-
note: `Naming settings applied, but no memberships schema was found for "${args.databaseName}" — sign-in may need a manual membership fixup.`,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const membershipsSchema = schemaResult.rows[0].schema_name;
|
|
121
|
-
await pool.query(`UPDATE "${membershipsSchema}".app_membership_defaults SET is_approved = TRUE, is_verified = TRUE`);
|
|
122
|
-
await pool.query(`UPDATE "${membershipsSchema}".app_memberships
|
|
123
|
-
SET is_approved = TRUE, is_verified = TRUE
|
|
124
|
-
WHERE is_approved = FALSE OR is_verified = FALSE`);
|
|
125
|
-
return { applied: true, note: `Membership defaults enabled (schema: ${membershipsSchema}).` };
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
129
|
-
return {
|
|
130
|
-
applied: false,
|
|
131
|
-
note: `Database provisioned, but SQL fixups failed (${message}). Sign-in/CRUD may fail until membership defaults are enabled manually.`,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
finally {
|
|
135
|
-
await pool?.end().catch(() => { });
|
|
136
|
-
}
|
|
137
|
-
}
|