@agentic-kit/pi 0.2.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/LICENSE +23 -0
- package/README.md +126 -0
- package/app-workspace.d.ts +16 -0
- package/app-workspace.js +145 -0
- package/confirm-gate.d.ts +13 -0
- package/confirm-gate.js +51 -0
- package/context.d.ts +39 -0
- package/context.js +251 -0
- package/db-probe.d.ts +29 -0
- package/db-probe.js +50 -0
- package/esm/app-workspace.d.ts +16 -0
- package/esm/app-workspace.js +134 -0
- package/esm/confirm-gate.d.ts +13 -0
- package/esm/confirm-gate.js +48 -0
- package/esm/context.d.ts +39 -0
- package/esm/context.js +242 -0
- package/esm/db-probe.d.ts +29 -0
- package/esm/db-probe.js +47 -0
- package/esm/host.d.ts +55 -0
- package/esm/host.js +20 -0
- package/esm/index.d.ts +11 -0
- package/esm/index.js +48 -0
- package/esm/policy/add-policies-to-table.d.ts +26 -0
- package/esm/policy/add-policies-to-table.js +66 -0
- package/esm/policy/provision-helpers.d.ts +38 -0
- package/esm/policy/provision-helpers.js +50 -0
- package/esm/provision-database/create-database-provision.d.ts +11 -0
- package/esm/provision-database/create-database-provision.js +48 -0
- package/esm/provision-database/credential.d.ts +14 -0
- package/esm/provision-database/credential.js +19 -0
- package/esm/provision-database/env-file.d.ts +37 -0
- package/esm/provision-database/env-file.js +85 -0
- package/esm/provision-database/modules.d.ts +2 -0
- package/esm/provision-database/modules.js +40 -0
- package/esm/provision-database/pg-fixups.d.ts +16 -0
- package/esm/provision-database/pg-fixups.js +132 -0
- package/esm/records/meta.d.ts +41 -0
- package/esm/records/meta.js +170 -0
- package/esm/run-codegen/barrels.d.ts +3 -0
- package/esm/run-codegen/barrels.js +48 -0
- package/esm/run-codegen/endpoints.d.ts +8 -0
- package/esm/run-codegen/endpoints.js +46 -0
- package/esm/schema-resolve.d.ts +23 -0
- package/esm/schema-resolve.js +69 -0
- package/esm/tool-schema.d.ts +3 -0
- package/esm/tool-schema.js +10 -0
- package/esm/tools/add-policies.d.ts +8 -0
- package/esm/tools/add-policies.js +96 -0
- package/esm/tools/add-records.d.ts +14 -0
- package/esm/tools/add-records.js +124 -0
- package/esm/tools/add-relation-schema.d.ts +30 -0
- package/esm/tools/add-relation-schema.js +58 -0
- package/esm/tools/add-relation.d.ts +10 -0
- package/esm/tools/add-relation.js +91 -0
- package/esm/tools/describe-schema.d.ts +18 -0
- package/esm/tools/describe-schema.js +132 -0
- package/esm/tools/mutations.d.ts +14 -0
- package/esm/tools/mutations.js +235 -0
- package/esm/tools/provision-blueprint.d.ts +22 -0
- package/esm/tools/provision-blueprint.js +111 -0
- package/esm/tools/provision-database.d.ts +14 -0
- package/esm/tools/provision-database.js +251 -0
- package/esm/tools/run-codegen.d.ts +11 -0
- package/esm/tools/run-codegen.js +139 -0
- package/esm/tools/templates.d.ts +49 -0
- package/esm/tools/templates.js +337 -0
- package/host.d.ts +55 -0
- package/host.js +25 -0
- package/index.d.ts +11 -0
- package/index.js +60 -0
- package/package.json +51 -0
- package/policy/add-policies-to-table.d.ts +26 -0
- package/policy/add-policies-to-table.js +71 -0
- package/policy/provision-helpers.d.ts +38 -0
- package/policy/provision-helpers.js +58 -0
- package/provision-database/create-database-provision.d.ts +11 -0
- package/provision-database/create-database-provision.js +51 -0
- package/provision-database/credential.d.ts +14 -0
- package/provision-database/credential.js +22 -0
- package/provision-database/env-file.d.ts +37 -0
- package/provision-database/env-file.js +91 -0
- package/provision-database/modules.d.ts +2 -0
- package/provision-database/modules.js +43 -0
- package/provision-database/pg-fixups.d.ts +16 -0
- package/provision-database/pg-fixups.js +170 -0
- package/records/meta.d.ts +41 -0
- package/records/meta.js +175 -0
- package/run-codegen/barrels.d.ts +3 -0
- package/run-codegen/barrels.js +54 -0
- package/run-codegen/endpoints.d.ts +8 -0
- package/run-codegen/endpoints.js +51 -0
- package/schema-resolve.d.ts +23 -0
- package/schema-resolve.js +75 -0
- package/tool-schema.d.ts +3 -0
- package/tool-schema.js +13 -0
- package/tools/add-policies.d.ts +8 -0
- package/tools/add-policies.js +99 -0
- package/tools/add-records.d.ts +14 -0
- package/tools/add-records.js +127 -0
- package/tools/add-relation-schema.d.ts +30 -0
- package/tools/add-relation-schema.js +62 -0
- package/tools/add-relation.d.ts +10 -0
- package/tools/add-relation.js +94 -0
- package/tools/describe-schema.d.ts +18 -0
- package/tools/describe-schema.js +135 -0
- package/tools/mutations.d.ts +14 -0
- package/tools/mutations.js +238 -0
- package/tools/provision-blueprint.d.ts +22 -0
- package/tools/provision-blueprint.js +114 -0
- package/tools/provision-database.d.ts +14 -0
- package/tools/provision-database.js +257 -0
- package/tools/run-codegen.d.ts +11 -0
- package/tools/run-codegen.js +143 -0
- package/tools/templates.d.ts +49 -0
- package/tools/templates.js +341 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Provision a database through the modules endpoint's createDatabaseProvisionModule
|
|
2
|
+
// mutation — an insert into database_provision_modules whose BEFORE INSERT trigger
|
|
3
|
+
// creates the database, domain, API, and module set before the row returns (the
|
|
4
|
+
// same contract constructive-client's admin app uses). The retired requestDatabase
|
|
5
|
+
// mutation only ever existed on dev builds of the api endpoint; production
|
|
6
|
+
// deployments (modules.launchql.dev) expose this one. bootstrapUser: true copies
|
|
7
|
+
// the owner into the new database so its per-DB API recognizes the account user.
|
|
8
|
+
// The generated ORM's default adapter is plain fetch, so the SDK's FetchAdapter is
|
|
9
|
+
// injected to keep *.localhost DNS/Host routing working in Node.
|
|
10
|
+
import { api, modules } from '@constructive-io/sdk';
|
|
11
|
+
export async function createDatabaseProvision(args) {
|
|
12
|
+
const db = modules.createClient({
|
|
13
|
+
adapter: new api.FetchAdapter(args.endpoint, {
|
|
14
|
+
Authorization: `Bearer ${args.bearer}`,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
const result = await db.databaseProvisionModule
|
|
18
|
+
.create({
|
|
19
|
+
data: {
|
|
20
|
+
databaseName: args.databaseName,
|
|
21
|
+
subdomain: args.databaseName,
|
|
22
|
+
domain: args.domain,
|
|
23
|
+
ownerId: args.ownerId,
|
|
24
|
+
modules: args.modules,
|
|
25
|
+
bootstrapUser: true,
|
|
26
|
+
},
|
|
27
|
+
select: {
|
|
28
|
+
id: true,
|
|
29
|
+
databaseId: true,
|
|
30
|
+
status: true,
|
|
31
|
+
errorMessage: true,
|
|
32
|
+
completedAt: true,
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
.unwrap();
|
|
36
|
+
const record = result.createDatabaseProvisionModule?.databaseProvisionModule;
|
|
37
|
+
if (!record)
|
|
38
|
+
throw new Error('createDatabaseProvisionModule returned no provision record.');
|
|
39
|
+
if (record.status === 'failed') {
|
|
40
|
+
throw new Error(record.errorMessage ?? 'provisioning failed');
|
|
41
|
+
}
|
|
42
|
+
if (record.status !== 'completed') {
|
|
43
|
+
throw new Error(record.errorMessage ?? `provisioning did not complete (status: ${record.status ?? 'unknown'})`);
|
|
44
|
+
}
|
|
45
|
+
if (!record.databaseId)
|
|
46
|
+
throw new Error('provisioning completed but returned no databaseId.');
|
|
47
|
+
return { databaseId: record.databaseId };
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AccountCredential = {
|
|
2
|
+
userId: string;
|
|
3
|
+
accessToken: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
};
|
|
6
|
+
export type ProvisionCredential = {
|
|
7
|
+
mode: 'account';
|
|
8
|
+
ownerId: string;
|
|
9
|
+
bearer: string;
|
|
10
|
+
} | {
|
|
11
|
+
mode: 'error';
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function selectProvisionCredential(account: AccountCredential | null | undefined): ProvisionCredential;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Pure: decide which credential authenticates a project's provisioning. Under the
|
|
2
|
+
// account gate a signed-in session is guaranteed, so every new project provisions
|
|
3
|
+
// UNDER the account (account-owned + enumerable): the database's owner_id is the
|
|
4
|
+
// account's own userId, and the account's API key (falling back to its session
|
|
5
|
+
// token) is the bearer that authenticates the provision mutation and lands in the
|
|
6
|
+
// project .env as ACCESS_TOKEN (refreshed on every provision run, including the
|
|
7
|
+
// skip path). A signed-in session with no usable bearer (keychain locked) is an
|
|
8
|
+
// explicit error, never a silent throwaway owner.
|
|
9
|
+
// Side-effect-free so the branch is unit-testable without runtime/network.
|
|
10
|
+
export function selectProvisionCredential(account) {
|
|
11
|
+
const bearer = account?.apiKey ?? account?.accessToken;
|
|
12
|
+
if (account?.userId && bearer) {
|
|
13
|
+
return { mode: 'account', ownerId: account.userId, bearer };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
mode: 'error',
|
|
17
|
+
reason: 'No usable account credential. Sign in (or unlock your session) before provisioning a database.',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type EnvVars = Record<string, string>;
|
|
2
|
+
/**
|
|
3
|
+
* Upsert `vars` into the existing `.env` `content`. Existing keys are replaced
|
|
4
|
+
* in place (preserving surrounding lines/comments); new keys are appended.
|
|
5
|
+
* Returns the new file body, trailing-newline-terminated.
|
|
6
|
+
*/
|
|
7
|
+
export declare function mergeEnv(content: string, vars: EnvVars): string;
|
|
8
|
+
/** The credential keys provision_database owns in a project's .env. */
|
|
9
|
+
export declare const PROVISION_ENV_KEYS: readonly ["DATABASE_ID", "DATABASE_NAME", "OWNER_ID", "ACCESS_TOKEN", "PGDATABASE", "API_ENDPOINT", "MODULES_ENDPOINT"];
|
|
10
|
+
/**
|
|
11
|
+
* Keys archived as comments on reprovision — the binding identity: which
|
|
12
|
+
* database, under whom, on which backend. ACCESS_TOKEN is the account login
|
|
13
|
+
* token, identical across bindings, so it is overwritten in place rather than
|
|
14
|
+
* archived.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ARCHIVED_BINDING_KEYS: readonly ["DATABASE_ID", "DATABASE_NAME", "OWNER_ID", "PGDATABASE", "API_ENDPOINT", "MODULES_ENDPOINT"];
|
|
17
|
+
/**
|
|
18
|
+
* Build the provisioning credential block written to a project's .env. The
|
|
19
|
+
* endpoint pins record which backend owns this binding — outside-app consumers
|
|
20
|
+
* (verify-feature.sh, skill scripts, dev-server shells) resolve them instead of
|
|
21
|
+
* falling back to localhost defaults when the app targets a hosted backend.
|
|
22
|
+
*/
|
|
23
|
+
export declare function provisionEnvVars(args: {
|
|
24
|
+
databaseId: string;
|
|
25
|
+
databaseName: string;
|
|
26
|
+
ownerId: string;
|
|
27
|
+
accessToken: string;
|
|
28
|
+
apiEndpoint: string;
|
|
29
|
+
modulesEndpoint: string;
|
|
30
|
+
}): EnvVars;
|
|
31
|
+
/**
|
|
32
|
+
* Comment out the given keys in place (`# archived <date>: KEY=value`) so a
|
|
33
|
+
* reprovision keeps the previous binding readable in the file instead of
|
|
34
|
+
* silently overwriting it. Every other line — including existing comments and
|
|
35
|
+
* already-archived entries — passes through untouched.
|
|
36
|
+
*/
|
|
37
|
+
export declare function archiveBindingKeys(content: string, keys: readonly string[], archivedOn: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Pure .env merge — used by provision_database to persist provisioning
|
|
2
|
+
// credentials without clobbering existing keys: replace a key in place if
|
|
3
|
+
// present, else append.
|
|
4
|
+
// Kept side-effect-free so it is unit-testable (the tool does the fs write).
|
|
5
|
+
/**
|
|
6
|
+
* Upsert `vars` into the existing `.env` `content`. Existing keys are replaced
|
|
7
|
+
* in place (preserving surrounding lines/comments); new keys are appended.
|
|
8
|
+
* Returns the new file body, trailing-newline-terminated.
|
|
9
|
+
*/
|
|
10
|
+
export function mergeEnv(content, vars) {
|
|
11
|
+
let out = content;
|
|
12
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
13
|
+
// Escape the key for the regex, and use a replacement function so `$`
|
|
14
|
+
// sequences in the value are never treated as replacement specials.
|
|
15
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
const regex = new RegExp(`^${escapedKey}=.*`, 'm');
|
|
17
|
+
if (regex.test(out))
|
|
18
|
+
out = out.replace(regex, () => `${key}=${val}`);
|
|
19
|
+
else
|
|
20
|
+
out += `\n${key}=${val}`;
|
|
21
|
+
}
|
|
22
|
+
return out.trim() + '\n';
|
|
23
|
+
}
|
|
24
|
+
/** The credential keys provision_database owns in a project's .env. */
|
|
25
|
+
export const PROVISION_ENV_KEYS = [
|
|
26
|
+
'DATABASE_ID',
|
|
27
|
+
'DATABASE_NAME',
|
|
28
|
+
'OWNER_ID',
|
|
29
|
+
'ACCESS_TOKEN',
|
|
30
|
+
'PGDATABASE',
|
|
31
|
+
'API_ENDPOINT',
|
|
32
|
+
'MODULES_ENDPOINT',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Keys archived as comments on reprovision — the binding identity: which
|
|
36
|
+
* database, under whom, on which backend. ACCESS_TOKEN is the account login
|
|
37
|
+
* token, identical across bindings, so it is overwritten in place rather than
|
|
38
|
+
* archived.
|
|
39
|
+
*/
|
|
40
|
+
export const ARCHIVED_BINDING_KEYS = [
|
|
41
|
+
'DATABASE_ID',
|
|
42
|
+
'DATABASE_NAME',
|
|
43
|
+
'OWNER_ID',
|
|
44
|
+
'PGDATABASE',
|
|
45
|
+
'API_ENDPOINT',
|
|
46
|
+
'MODULES_ENDPOINT',
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Build the provisioning credential block written to a project's .env. The
|
|
50
|
+
* endpoint pins record which backend owns this binding — outside-app consumers
|
|
51
|
+
* (verify-feature.sh, skill scripts, dev-server shells) resolve them instead of
|
|
52
|
+
* falling back to localhost defaults when the app targets a hosted backend.
|
|
53
|
+
*/
|
|
54
|
+
export function provisionEnvVars(args) {
|
|
55
|
+
return {
|
|
56
|
+
DATABASE_ID: args.databaseId,
|
|
57
|
+
DATABASE_NAME: args.databaseName,
|
|
58
|
+
OWNER_ID: args.ownerId,
|
|
59
|
+
ACCESS_TOKEN: args.accessToken,
|
|
60
|
+
PGDATABASE: args.databaseName,
|
|
61
|
+
API_ENDPOINT: args.apiEndpoint,
|
|
62
|
+
MODULES_ENDPOINT: args.modulesEndpoint,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Comment out the given keys in place (`# archived <date>: KEY=value`) so a
|
|
67
|
+
* reprovision keeps the previous binding readable in the file instead of
|
|
68
|
+
* silently overwriting it. Every other line — including existing comments and
|
|
69
|
+
* already-archived entries — passes through untouched.
|
|
70
|
+
*/
|
|
71
|
+
export function archiveBindingKeys(content, keys, archivedOn) {
|
|
72
|
+
const keySet = new Set(keys);
|
|
73
|
+
return content
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map((line) => {
|
|
76
|
+
if (line.trimStart().startsWith('#'))
|
|
77
|
+
return line;
|
|
78
|
+
const eq = line.indexOf('=');
|
|
79
|
+
if (eq === -1)
|
|
80
|
+
return line;
|
|
81
|
+
const key = line.slice(0, eq).trim();
|
|
82
|
+
return keySet.has(key) ? `# archived ${archivedOn}: ${line.trim()}` : line;
|
|
83
|
+
})
|
|
84
|
+
.join('\n');
|
|
85
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const DEFAULT_PROVISION_MODULES = [
|
|
2
|
+
'users_module',
|
|
3
|
+
'membership_types_module',
|
|
4
|
+
['permissions_module', { scope: 'app' }],
|
|
5
|
+
['permissions_module', { scope: 'org' }],
|
|
6
|
+
['limits_module', { scope: 'app' }],
|
|
7
|
+
['limits_module', { scope: 'org' }],
|
|
8
|
+
['levels_module', { scope: 'app' }],
|
|
9
|
+
['levels_module', { scope: 'org' }],
|
|
10
|
+
['memberships_module', { scope: 'app' }],
|
|
11
|
+
['memberships_module', { scope: 'org' }],
|
|
12
|
+
'sessions_module',
|
|
13
|
+
'user_state_module',
|
|
14
|
+
'user_credentials_module',
|
|
15
|
+
['internal_secrets_module', { scope: 'app' }],
|
|
16
|
+
'emails_module',
|
|
17
|
+
'rls_module',
|
|
18
|
+
'user_auth_module',
|
|
19
|
+
'session_secrets_module',
|
|
20
|
+
'rate_limits_module',
|
|
21
|
+
'connected_accounts_module',
|
|
22
|
+
['identity_providers_module', { scope: 'app' }],
|
|
23
|
+
'webauthn_credentials_module',
|
|
24
|
+
'webauthn_auth_module',
|
|
25
|
+
'phone_numbers_module',
|
|
26
|
+
['profiles_module', { scope: 'app' }],
|
|
27
|
+
['profiles_module', { scope: 'org' }],
|
|
28
|
+
['hierarchy_module', { scope: 'org' }],
|
|
29
|
+
['invites_module', { scope: 'app' }],
|
|
30
|
+
['invites_module', { scope: 'org' }],
|
|
31
|
+
// Mirrors constructive-client: api_name links storage to the same api-<slug>
|
|
32
|
+
// endpoint the generated SDK targets (without it uploads fail with "not
|
|
33
|
+
// enabled for this endpoint"), prefix keeps app_buckets/app_files names off
|
|
34
|
+
// user tables, and buckets seeds the public bucket row uploads need.
|
|
35
|
+
[
|
|
36
|
+
'storage_module',
|
|
37
|
+
{ scope: 'app', api_name: 'api', prefix: 'app', buckets: [{ key: 'public', is_public: true }] },
|
|
38
|
+
],
|
|
39
|
+
'devices_module',
|
|
40
|
+
];
|
|
@@ -0,0 +1,16 @@
|
|
|
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>;
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Table, TableInflection } from '@constructive-io/graphql-query/types/schema';
|
|
2
|
+
export declare const META_QUERY = "\n query DbToolsMeta {\n _meta {\n tables {\n name\n query { all one create update delete }\n inflection {\n allRows\n conditionType\n connection\n createInputType\n createPayloadType\n deletePayloadType\n edge\n filterType\n orderByType\n patchType\n tableType\n updatePayloadType\n }\n fields {\n name\n isNotNull\n hasDefault\n type {\n gqlType\n isArray\n pgType\n subtype\n isNotNull\n hasDefault\n }\n }\n }\n }\n }\n";
|
|
3
|
+
type RawType = {
|
|
4
|
+
gqlType: string;
|
|
5
|
+
isArray: boolean;
|
|
6
|
+
modifier?: string | number | null;
|
|
7
|
+
pgAlias?: string | null;
|
|
8
|
+
pgType?: string | null;
|
|
9
|
+
subtype?: string | null;
|
|
10
|
+
typmod?: number | null;
|
|
11
|
+
isNotNull?: boolean | null;
|
|
12
|
+
hasDefault?: boolean | null;
|
|
13
|
+
};
|
|
14
|
+
type RawField = {
|
|
15
|
+
name: string;
|
|
16
|
+
isNotNull?: boolean | null;
|
|
17
|
+
hasDefault?: boolean | null;
|
|
18
|
+
type: RawType;
|
|
19
|
+
};
|
|
20
|
+
type RawInflection = Partial<Record<keyof TableInflection, string | null>>;
|
|
21
|
+
type RawQuery = {
|
|
22
|
+
all: string;
|
|
23
|
+
one?: string | null;
|
|
24
|
+
create?: string | null;
|
|
25
|
+
update?: string | null;
|
|
26
|
+
delete?: string | null;
|
|
27
|
+
};
|
|
28
|
+
export type RawMetaTable = {
|
|
29
|
+
name: string;
|
|
30
|
+
query?: RawQuery | null;
|
|
31
|
+
inflection?: RawInflection | null;
|
|
32
|
+
fields?: (RawField | null)[] | null;
|
|
33
|
+
};
|
|
34
|
+
export type MetaResponse = {
|
|
35
|
+
_meta?: {
|
|
36
|
+
tables?: (RawMetaTable | null)[] | null;
|
|
37
|
+
} | null;
|
|
38
|
+
};
|
|
39
|
+
export declare function cleanTable(meta: RawMetaTable): Table;
|
|
40
|
+
export declare function findMetaTable(tables: RawMetaTable[], requested: string): RawMetaTable | undefined;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Vendored from constructive-client's `@constructive-io/data` (not published to
|
|
2
|
+
// the registry). We query the per-DB data endpoint's `_meta` introspection and
|
|
3
|
+
// reshape each table into the `Table` shape `buildPostGraphileCreate` expects.
|
|
4
|
+
// Relations are intentionally left empty: the create-mutation builder only reads
|
|
5
|
+
// them to skip relational fields in the return selection, and every `_meta` field
|
|
6
|
+
// is a real scalar column, so selecting them all is valid.
|
|
7
|
+
//
|
|
8
|
+
// Only fields the live backend's `_meta` actually exposes are selected here —
|
|
9
|
+
// over-selecting (e.g. MetaType.modifier or MetaInflection.createField) fails
|
|
10
|
+
// GraphQL validation. The reshapers below tolerate the omitted fields.
|
|
11
|
+
export const META_QUERY = `
|
|
12
|
+
query DbToolsMeta {
|
|
13
|
+
_meta {
|
|
14
|
+
tables {
|
|
15
|
+
name
|
|
16
|
+
query { all one create update delete }
|
|
17
|
+
inflection {
|
|
18
|
+
allRows
|
|
19
|
+
conditionType
|
|
20
|
+
connection
|
|
21
|
+
createInputType
|
|
22
|
+
createPayloadType
|
|
23
|
+
deletePayloadType
|
|
24
|
+
edge
|
|
25
|
+
filterType
|
|
26
|
+
orderByType
|
|
27
|
+
patchType
|
|
28
|
+
tableType
|
|
29
|
+
updatePayloadType
|
|
30
|
+
}
|
|
31
|
+
fields {
|
|
32
|
+
name
|
|
33
|
+
isNotNull
|
|
34
|
+
hasDefault
|
|
35
|
+
type {
|
|
36
|
+
gqlType
|
|
37
|
+
isArray
|
|
38
|
+
pgType
|
|
39
|
+
subtype
|
|
40
|
+
isNotNull
|
|
41
|
+
hasDefault
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
function pgFieldToCamelCase(name) {
|
|
49
|
+
return name.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
50
|
+
}
|
|
51
|
+
function convertInflection(inflection) {
|
|
52
|
+
if (!inflection)
|
|
53
|
+
return undefined;
|
|
54
|
+
const s = (key) => inflection[key] ?? '';
|
|
55
|
+
const n = (key) => inflection[key] ?? null;
|
|
56
|
+
return {
|
|
57
|
+
allRows: s('allRows'),
|
|
58
|
+
allRowsSimple: s('allRowsSimple'),
|
|
59
|
+
conditionType: s('conditionType'),
|
|
60
|
+
connection: s('connection'),
|
|
61
|
+
createField: s('createField'),
|
|
62
|
+
createInputType: s('createInputType'),
|
|
63
|
+
createPayloadType: s('createPayloadType'),
|
|
64
|
+
deleteByPrimaryKey: n('deleteByPrimaryKey'),
|
|
65
|
+
deletePayloadType: s('deletePayloadType'),
|
|
66
|
+
edge: s('edge'),
|
|
67
|
+
edgeField: s('edgeField'),
|
|
68
|
+
enumType: s('enumType'),
|
|
69
|
+
filterType: n('filterType'),
|
|
70
|
+
inputType: s('inputType'),
|
|
71
|
+
orderByType: s('orderByType'),
|
|
72
|
+
patchField: s('patchField'),
|
|
73
|
+
patchType: n('patchType'),
|
|
74
|
+
tableFieldName: s('tableFieldName'),
|
|
75
|
+
tableType: s('tableType'),
|
|
76
|
+
typeName: s('typeName'),
|
|
77
|
+
updateByPrimaryKey: n('updateByPrimaryKey'),
|
|
78
|
+
updatePayloadType: n('updatePayloadType'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function convertQuery(query) {
|
|
82
|
+
if (!query)
|
|
83
|
+
return undefined;
|
|
84
|
+
return {
|
|
85
|
+
all: query.all,
|
|
86
|
+
one: query.one ?? null,
|
|
87
|
+
create: query.create ?? '',
|
|
88
|
+
update: query.update ?? null,
|
|
89
|
+
delete: query.delete ?? null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function cleanTable(meta) {
|
|
93
|
+
return {
|
|
94
|
+
name: meta.name,
|
|
95
|
+
inflection: convertInflection(meta.inflection),
|
|
96
|
+
query: convertQuery(meta.query),
|
|
97
|
+
fields: (meta.fields ?? [])
|
|
98
|
+
.filter((f) => Boolean(f))
|
|
99
|
+
.map((field) => ({
|
|
100
|
+
name: pgFieldToCamelCase(field.name),
|
|
101
|
+
type: {
|
|
102
|
+
gqlType: field.type.gqlType,
|
|
103
|
+
isArray: field.type.isArray,
|
|
104
|
+
modifier: field.type.modifier,
|
|
105
|
+
pgAlias: field.type.pgAlias,
|
|
106
|
+
pgType: field.type.pgType,
|
|
107
|
+
subtype: field.type.subtype,
|
|
108
|
+
typmod: field.type.typmod,
|
|
109
|
+
},
|
|
110
|
+
isNotNull: field.isNotNull ?? field.type.isNotNull ?? null,
|
|
111
|
+
hasDefault: field.hasDefault ?? field.type.hasDefault ?? null,
|
|
112
|
+
})),
|
|
113
|
+
relations: { belongsTo: [], hasOne: [], hasMany: [], manyToMany: [] },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function normalizeTableLookupKey(value) {
|
|
117
|
+
return (value ?? '')
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.replace(/[^a-z0-9]/g, '');
|
|
121
|
+
}
|
|
122
|
+
function getTableLookupVariants(value) {
|
|
123
|
+
const normalized = normalizeTableLookupKey(value);
|
|
124
|
+
if (!normalized)
|
|
125
|
+
return [];
|
|
126
|
+
const variants = new Set([normalized]);
|
|
127
|
+
if (normalized.endsWith('y')) {
|
|
128
|
+
variants.add(`${normalized}s`);
|
|
129
|
+
variants.add(`${normalized.slice(0, -1)}ies`);
|
|
130
|
+
}
|
|
131
|
+
if (normalized.endsWith('ies')) {
|
|
132
|
+
variants.add(`${normalized.slice(0, -3)}y`);
|
|
133
|
+
}
|
|
134
|
+
if (/(s|x|z|ch|sh)$/.test(normalized))
|
|
135
|
+
variants.add(`${normalized}es`);
|
|
136
|
+
if (normalized.endsWith('es'))
|
|
137
|
+
variants.add(normalized.slice(0, -2));
|
|
138
|
+
if (!normalized.endsWith('s'))
|
|
139
|
+
variants.add(`${normalized}s`);
|
|
140
|
+
if (normalized.endsWith('s'))
|
|
141
|
+
variants.add(normalized.slice(0, -1));
|
|
142
|
+
return [...variants].filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
// Resolve a user-supplied table name (snake_case singular or plural) against the
|
|
145
|
+
// PascalCase-singular `_meta` table names, tolerating pluralization differences.
|
|
146
|
+
export function findMetaTable(tables, requested) {
|
|
147
|
+
const index = new Map();
|
|
148
|
+
for (const table of tables) {
|
|
149
|
+
const keys = [
|
|
150
|
+
table.name,
|
|
151
|
+
table.query?.all,
|
|
152
|
+
table.query?.one,
|
|
153
|
+
table.inflection?.tableType,
|
|
154
|
+
table.inflection?.tableFieldName,
|
|
155
|
+
table.inflection?.allRows,
|
|
156
|
+
];
|
|
157
|
+
for (const key of keys) {
|
|
158
|
+
for (const variant of getTableLookupVariants(key)) {
|
|
159
|
+
if (!index.has(variant))
|
|
160
|
+
index.set(variant, table);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const variant of getTableLookupVariants(requested)) {
|
|
165
|
+
const match = index.get(variant);
|
|
166
|
+
if (match)
|
|
167
|
+
return match;
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Turbopack barrel normalization (TURBOPACK-BARREL-001).
|
|
2
|
+
//
|
|
3
|
+
// Codegen emits directory barrel re-exports like `export * from './hooks'` where
|
|
4
|
+
// `./hooks` is a directory with an index.ts. Next.js 16's Turbopack cannot
|
|
5
|
+
// resolve directory specifiers — it needs an explicit `./hooks/index`. This pass
|
|
6
|
+
// rewrites `./X` -> `./X/index` only when X resolves to a sibling DIRECTORY,
|
|
7
|
+
// leaving file specifiers (and ones already ending in /index) untouched.
|
|
8
|
+
//
|
|
9
|
+
// Pure-ish: operates on a real directory tree (so it's exercised with a temp
|
|
10
|
+
// fixture in tests) and is idempotent — safe to re-run.
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
const SPEC_RE = /((?:export|import)[^\n]*?from\s*["'])(\.\.?\/[^"']+?)(["'])/g;
|
|
14
|
+
const FILE_EXT_RE = /\.(tsx?|jsx?|json|css)$/;
|
|
15
|
+
/** Rewrite directory barrel specifiers under `root` in place. Returns the count
|
|
16
|
+
* of files changed. No-op (returns 0) when `root` does not exist. */
|
|
17
|
+
export function normalizeSdkBarrels(root) {
|
|
18
|
+
if (!fs.existsSync(root))
|
|
19
|
+
return 0;
|
|
20
|
+
let changed = 0;
|
|
21
|
+
const walk = (dir) => {
|
|
22
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const p = path.join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
walk(p);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!/\.tsx?$/.test(entry.name))
|
|
29
|
+
continue;
|
|
30
|
+
const orig = fs.readFileSync(p, 'utf8');
|
|
31
|
+
const next = orig.replace(SPEC_RE, (match, pre, spec, post) => {
|
|
32
|
+
if (FILE_EXT_RE.test(spec))
|
|
33
|
+
return match; // already a file
|
|
34
|
+
const abs = path.resolve(path.dirname(p), spec);
|
|
35
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
|
|
36
|
+
return pre + spec.replace(/\/$/, '') + '/index' + post;
|
|
37
|
+
}
|
|
38
|
+
return match;
|
|
39
|
+
});
|
|
40
|
+
if (next !== orig) {
|
|
41
|
+
fs.writeFileSync(p, next);
|
|
42
|
+
changed++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
walk(root);
|
|
47
|
+
return changed;
|
|
48
|
+
}
|