@agentic-kit/db-tools 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.
Files changed (131) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +138 -0
  3. package/app-workspace.d.ts +16 -0
  4. package/app-workspace.js +145 -0
  5. package/context.d.ts +62 -0
  6. package/context.js +282 -0
  7. package/db-probe.d.ts +30 -0
  8. package/db-probe.js +50 -0
  9. package/esm/app-workspace.d.ts +16 -0
  10. package/esm/app-workspace.js +134 -0
  11. package/esm/context.d.ts +62 -0
  12. package/esm/context.js +270 -0
  13. package/esm/db-probe.d.ts +30 -0
  14. package/esm/db-probe.js +47 -0
  15. package/esm/host.d.ts +115 -0
  16. package/esm/host.js +23 -0
  17. package/esm/index.d.ts +20 -0
  18. package/esm/index.js +52 -0
  19. package/esm/policy/add-policies-to-table.d.ts +26 -0
  20. package/esm/policy/add-policies-to-table.js +66 -0
  21. package/esm/policy/provision-helpers.d.ts +38 -0
  22. package/esm/policy/provision-helpers.js +50 -0
  23. package/esm/provision-database/credential.d.ts +14 -0
  24. package/esm/provision-database/credential.js +19 -0
  25. package/esm/provision-database/env-file.d.ts +37 -0
  26. package/esm/provision-database/env-file.js +85 -0
  27. package/esm/provision-database/manifest.d.ts +10 -0
  28. package/esm/provision-database/manifest.js +48 -0
  29. package/esm/provision-database/pg-fixups.d.ts +16 -0
  30. package/esm/provision-database/pg-fixups.js +132 -0
  31. package/esm/provision-database/preset-match.d.ts +13 -0
  32. package/esm/provision-database/preset-match.js +66 -0
  33. package/esm/provision-database/presets.d.ts +4 -0
  34. package/esm/provision-database/presets.js +13 -0
  35. package/esm/provision-database/request-database.d.ts +61 -0
  36. package/esm/provision-database/request-database.js +130 -0
  37. package/esm/provision-database/resolve.d.ts +27 -0
  38. package/esm/provision-database/resolve.js +49 -0
  39. package/esm/records/meta.d.ts +41 -0
  40. package/esm/records/meta.js +167 -0
  41. package/esm/run-codegen/barrels.d.ts +3 -0
  42. package/esm/run-codegen/barrels.js +48 -0
  43. package/esm/run-codegen/endpoints.d.ts +8 -0
  44. package/esm/run-codegen/endpoints.js +46 -0
  45. package/esm/schema-resolve.d.ts +23 -0
  46. package/esm/schema-resolve.js +69 -0
  47. package/esm/tool-schema.d.ts +3 -0
  48. package/esm/tool-schema.js +10 -0
  49. package/esm/tools/add-policies.d.ts +24 -0
  50. package/esm/tools/add-policies.js +94 -0
  51. package/esm/tools/add-records.d.ts +18 -0
  52. package/esm/tools/add-records.js +122 -0
  53. package/esm/tools/add-relation-schema.d.ts +29 -0
  54. package/esm/tools/add-relation-schema.js +56 -0
  55. package/esm/tools/add-relation.d.ts +10 -0
  56. package/esm/tools/add-relation.js +91 -0
  57. package/esm/tools/create-api-key.d.ts +25 -0
  58. package/esm/tools/create-api-key.js +214 -0
  59. package/esm/tools/describe-schema.d.ts +19 -0
  60. package/esm/tools/describe-schema.js +130 -0
  61. package/esm/tools/manage-entity-types.d.ts +40 -0
  62. package/esm/tools/manage-entity-types.js +201 -0
  63. package/esm/tools/mutations.d.ts +35 -0
  64. package/esm/tools/mutations.js +230 -0
  65. package/esm/tools/provision-blueprint.d.ts +21 -0
  66. package/esm/tools/provision-blueprint.js +109 -0
  67. package/esm/tools/provision-database.d.ts +18 -0
  68. package/esm/tools/provision-database.js +260 -0
  69. package/esm/tools/run-codegen.d.ts +12 -0
  70. package/esm/tools/run-codegen.js +137 -0
  71. package/esm/tools/templates.d.ts +71 -0
  72. package/esm/tools/templates.js +331 -0
  73. package/host.d.ts +115 -0
  74. package/host.js +28 -0
  75. package/index.d.ts +20 -0
  76. package/index.js +76 -0
  77. package/package.json +49 -0
  78. package/policy/add-policies-to-table.d.ts +26 -0
  79. package/policy/add-policies-to-table.js +71 -0
  80. package/policy/provision-helpers.d.ts +38 -0
  81. package/policy/provision-helpers.js +58 -0
  82. package/provision-database/credential.d.ts +14 -0
  83. package/provision-database/credential.js +22 -0
  84. package/provision-database/env-file.d.ts +37 -0
  85. package/provision-database/env-file.js +91 -0
  86. package/provision-database/manifest.d.ts +10 -0
  87. package/provision-database/manifest.js +56 -0
  88. package/provision-database/pg-fixups.d.ts +16 -0
  89. package/provision-database/pg-fixups.js +170 -0
  90. package/provision-database/preset-match.d.ts +13 -0
  91. package/provision-database/preset-match.js +70 -0
  92. package/provision-database/presets.d.ts +4 -0
  93. package/provision-database/presets.js +17 -0
  94. package/provision-database/request-database.d.ts +61 -0
  95. package/provision-database/request-database.js +134 -0
  96. package/provision-database/resolve.d.ts +27 -0
  97. package/provision-database/resolve.js +53 -0
  98. package/records/meta.d.ts +41 -0
  99. package/records/meta.js +172 -0
  100. package/run-codegen/barrels.d.ts +3 -0
  101. package/run-codegen/barrels.js +54 -0
  102. package/run-codegen/endpoints.d.ts +8 -0
  103. package/run-codegen/endpoints.js +51 -0
  104. package/schema-resolve.d.ts +23 -0
  105. package/schema-resolve.js +75 -0
  106. package/tool-schema.d.ts +3 -0
  107. package/tool-schema.js +13 -0
  108. package/tools/add-policies.d.ts +24 -0
  109. package/tools/add-policies.js +97 -0
  110. package/tools/add-records.d.ts +18 -0
  111. package/tools/add-records.js +125 -0
  112. package/tools/add-relation-schema.d.ts +29 -0
  113. package/tools/add-relation-schema.js +60 -0
  114. package/tools/add-relation.d.ts +10 -0
  115. package/tools/add-relation.js +94 -0
  116. package/tools/create-api-key.d.ts +25 -0
  117. package/tools/create-api-key.js +220 -0
  118. package/tools/describe-schema.d.ts +19 -0
  119. package/tools/describe-schema.js +133 -0
  120. package/tools/manage-entity-types.d.ts +40 -0
  121. package/tools/manage-entity-types.js +206 -0
  122. package/tools/mutations.d.ts +35 -0
  123. package/tools/mutations.js +233 -0
  124. package/tools/provision-blueprint.d.ts +21 -0
  125. package/tools/provision-blueprint.js +112 -0
  126. package/tools/provision-database.d.ts +18 -0
  127. package/tools/provision-database.js +266 -0
  128. package/tools/run-codegen.d.ts +12 -0
  129. package/tools/run-codegen.js +141 -0
  130. package/tools/templates.d.ts +71 -0
  131. package/tools/templates.js +335 -0
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.derivePlaneEndpoints = derivePlaneEndpoints;
4
+ exports.codegenEndpointEnv = codegenEndpointEnv;
5
+ exports.envLocalContent = envLocalContent;
6
+ // Codegen and the Next.js dev server default every per-DB vhost to
7
+ // *-<db>.localhost:3000, which only exists on a local backend. Derive the real
8
+ // vhosts from the api endpoint the binding belongs to by swapping its first
9
+ // host label (api.launchql.dev -> admin-<db>.launchql.dev) — the same scheme
10
+ // context.ts uses for the data plane. The app DATA plane is `api-<db>`, not
11
+ // `app-<db>` (SUBDOMAIN-001).
12
+ function derivePlaneEndpoints(apiEndpoint, databaseName) {
13
+ const derive = (label) => {
14
+ try {
15
+ const url = new URL(apiEndpoint);
16
+ const labels = url.hostname.split('.');
17
+ labels[0] = `${label}-${databaseName}`;
18
+ url.hostname = labels.join('.');
19
+ return url.toString();
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ };
25
+ const admin = derive('admin');
26
+ const auth = derive('auth');
27
+ const app = derive('api');
28
+ return admin && auth && app ? { admin, auth, app } : undefined;
29
+ }
30
+ // The template's graphql-codegen.config.ts honors CODEGEN_*_ENDPOINT for the
31
+ // URL and CODEGEN_*_HOST for the Host header; set both so scheme and routing
32
+ // stay in sync on any backend.
33
+ function codegenEndpointEnv(planes) {
34
+ return {
35
+ CODEGEN_ADMIN_ENDPOINT: planes.admin,
36
+ CODEGEN_ADMIN_HOST: new URL(planes.admin).host,
37
+ CODEGEN_AUTH_ENDPOINT: planes.auth,
38
+ CODEGEN_AUTH_HOST: new URL(planes.auth).host,
39
+ CODEGEN_APP_ENDPOINT: planes.app,
40
+ CODEGEN_APP_HOST: new URL(planes.app).host,
41
+ };
42
+ }
43
+ // .env.local for the dev server: the template's runtime config falls back to
44
+ // localhost vhosts unless NEXT_PUBLIC_*_ENDPOINT overrides are present.
45
+ function envLocalContent(databaseName, planes) {
46
+ const lines = [`NEXT_PUBLIC_DB_NAME=${databaseName}`];
47
+ if (planes) {
48
+ lines.push(`NEXT_PUBLIC_ADMIN_ENDPOINT=${planes.admin}`, `NEXT_PUBLIC_AUTH_ENDPOINT=${planes.auth}`, `NEXT_PUBLIC_APP_ENDPOINT=${planes.app}`);
49
+ }
50
+ return `${lines.join('\n')}\n`;
51
+ }
@@ -0,0 +1,23 @@
1
+ import type { ProjectContext } from './context';
2
+ export type ResolvedField = {
3
+ id: string;
4
+ name: string;
5
+ tableId: string;
6
+ };
7
+ export type ResolvedUniqueConstraint = {
8
+ id: string;
9
+ fieldIds: string[];
10
+ };
11
+ export type ResolvedTable = {
12
+ id: string;
13
+ name: string;
14
+ fields: ResolvedField[];
15
+ uniqueConstraints: ResolvedUniqueConstraint[];
16
+ };
17
+ export type ResolvedSchema = {
18
+ tables: ResolvedTable[];
19
+ };
20
+ export declare function resolveSchema(context: ProjectContext): Promise<ResolvedSchema>;
21
+ export declare function resolveTable(schema: ResolvedSchema, tableName: string): ResolvedTable;
22
+ export declare function resolveField(table: ResolvedTable, fieldName: string): ResolvedField;
23
+ export declare function findUniqueConstraintId(table: ResolvedTable, fieldId: string): string | undefined;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveSchema = resolveSchema;
4
+ exports.resolveTable = resolveTable;
5
+ exports.resolveField = resolveField;
6
+ exports.findUniqueConstraintId = findUniqueConstraintId;
7
+ async function resolveSchema(context) {
8
+ const { api, databaseId } = context;
9
+ const tablesResult = await api.table
10
+ .findMany({
11
+ select: { id: true, name: true },
12
+ where: { databaseId: { equalTo: databaseId } },
13
+ })
14
+ .execute();
15
+ if (!tablesResult.ok) {
16
+ const message = tablesResult.errors.map((e) => e.message).join('; ');
17
+ throw new Error(`Failed to read tables: ${message}`);
18
+ }
19
+ const tableNodes = tablesResult.data.tables?.nodes ?? [];
20
+ const tableIds = tableNodes.map((t) => t.id);
21
+ const fieldsByTable = new Map();
22
+ const constraintsByTable = new Map();
23
+ if (tableIds.length > 0) {
24
+ const fieldsResult = await api.field
25
+ .findMany({
26
+ select: { id: true, name: true, tableId: true },
27
+ where: { tableId: { in: tableIds } },
28
+ })
29
+ .execute();
30
+ if (!fieldsResult.ok) {
31
+ const message = fieldsResult.errors.map((e) => e.message).join('; ');
32
+ throw new Error(`Failed to read fields: ${message}`);
33
+ }
34
+ for (const field of fieldsResult.data.fields?.nodes ?? []) {
35
+ const list = fieldsByTable.get(field.tableId) ?? [];
36
+ list.push({ id: field.id, name: field.name, tableId: field.tableId });
37
+ fieldsByTable.set(field.tableId, list);
38
+ }
39
+ const constraintsResult = await api.uniqueConstraint
40
+ .findMany({
41
+ select: { id: true, tableId: true, fieldIds: true },
42
+ where: { tableId: { in: tableIds } },
43
+ })
44
+ .execute();
45
+ if (constraintsResult.ok) {
46
+ for (const c of constraintsResult.data.uniqueConstraints?.nodes ?? []) {
47
+ const list = constraintsByTable.get(c.tableId) ?? [];
48
+ list.push({ id: c.id, fieldIds: Array.isArray(c.fieldIds) ? c.fieldIds : [] });
49
+ constraintsByTable.set(c.tableId, list);
50
+ }
51
+ }
52
+ }
53
+ const tables = tableNodes.map((table) => ({
54
+ id: table.id,
55
+ name: table.name,
56
+ fields: fieldsByTable.get(table.id) ?? [],
57
+ uniqueConstraints: constraintsByTable.get(table.id) ?? [],
58
+ }));
59
+ return { tables };
60
+ }
61
+ function resolveTable(schema, tableName) {
62
+ const table = schema.tables.find((t) => t.name === tableName);
63
+ if (!table)
64
+ throw new Error(`Table "${tableName}" not found in current schema`);
65
+ return table;
66
+ }
67
+ function resolveField(table, fieldName) {
68
+ const field = table.fields.find((f) => f.name === fieldName);
69
+ if (!field)
70
+ throw new Error(`Field "${fieldName}" not found in table "${table.name}"`);
71
+ return field;
72
+ }
73
+ function findUniqueConstraintId(table, fieldId) {
74
+ return table.uniqueConstraints.find((c) => c.fieldIds.includes(fieldId))?.id;
75
+ }
@@ -0,0 +1,3 @@
1
+ import type { TSchema } from 'typebox';
2
+ import { z } from 'zod';
3
+ export declare function toolSchema(schema: z.ZodType): TSchema;
package/tool-schema.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toolSchema = toolSchema;
4
+ const zod_1 = require("zod");
5
+ // Tool parameter schemas are authored in zod; pi's ToolDefinition wants a
6
+ // typebox TSchema. Structurally a TSchema is just a JSON Schema object, so the
7
+ // draft-7 output of z.toJSONSchema (minus the $schema marker, matching what
8
+ // typebox emits) is cast across the boundary. typebox itself is a type-only
9
+ // import — it never appears in the runtime bundle.
10
+ function toolSchema(schema) {
11
+ const { $schema: _$schema, ...rest } = zod_1.z.toJSONSchema(schema, { target: 'draft-7' });
12
+ return rest;
13
+ }
@@ -0,0 +1,24 @@
1
+ import { type HarnessTool } from '@agentic-kit/harness';
2
+ import { z } from 'zod';
3
+ declare const AddPoliciesZod: z.ZodObject<{
4
+ table_name: z.ZodString;
5
+ policies: z.ZodArray<z.ZodObject<{
6
+ policy_type: z.ZodString;
7
+ operations: z.ZodArray<z.ZodEnum<{
8
+ create: "create";
9
+ read: "read";
10
+ update: "update";
11
+ delete: "delete";
12
+ }>>;
13
+ role_name: z.ZodOptional<z.ZodString>;
14
+ is_permissive: z.ZodOptional<z.ZodBoolean>;
15
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
16
+ node_data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
17
+ }, z.core.$strip>>;
18
+ }, z.core.$strip>;
19
+ export type AddPoliciesDetails = {
20
+ success: boolean;
21
+ message: string;
22
+ };
23
+ export declare const addPoliciesTool: HarnessTool<typeof AddPoliciesZod, AddPoliciesDetails>;
24
+ export {};
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addPoliciesTool = void 0;
4
+ const harness_1 = require("@agentic-kit/harness");
5
+ const zod_1 = require("zod");
6
+ const context_1 = require("../context");
7
+ const add_policies_to_table_1 = require("../policy/add-policies-to-table");
8
+ const schema_resolve_1 = require("../schema-resolve");
9
+ const DEFAULT_ROLE = 'authenticated';
10
+ const CrudOperationSchema = zod_1.z.enum(['create', 'read', 'update', 'delete']);
11
+ const PolicyEntrySchema = zod_1.z.object({
12
+ policy_type: zod_1.z
13
+ .string()
14
+ .describe('Policy type id. Supported: AuthzAllowAll, AuthzDenyAll, AuthzAppMembership, AuthzDirectOwner, AuthzEntityMembership, AuthzOrgHierarchy, AuthzPublishable (these auto-create columns via their Data* node), AuthzRelatedMemberList, AuthzRelatedEntityMembership. Not supported (use the policies UI): AuthzDirectOwnerAny, AuthzMemberList, AuthzTemporal, AuthzComposite.'),
15
+ operations: zod_1.z
16
+ .array(CrudOperationSchema)
17
+ .min(1)
18
+ .describe('CRUD operations the policy applies to: any subset of ["create","read","update","delete"].'),
19
+ role_name: zod_1.z
20
+ .string()
21
+ .describe('PostgreSQL role the policy applies to. Defaults to "authenticated".')
22
+ .optional(),
23
+ is_permissive: zod_1.z
24
+ .boolean()
25
+ .describe('PERMISSIVE (default true) vs RESTRICTIVE — most policies should be permissive.')
26
+ .optional(),
27
+ data: zod_1.z
28
+ .record(zod_1.z.string(), zod_1.z.unknown())
29
+ .describe('Type-specific shared data. AuthzDirectOwner: { entity_field: "owner_id" }. AuthzAppMembership: { permission?, is_admin?, is_owner? }. Empty object {} for AuthzAllowAll / AuthzDenyAll.')
30
+ .optional(),
31
+ node_data: zod_1.z
32
+ .record(zod_1.z.string(), zod_1.z.unknown())
33
+ .describe('Optional data for the underlying Data* node (has-module policies only).')
34
+ .optional(),
35
+ });
36
+ const AddPoliciesZod = zod_1.z.object({
37
+ table_name: zod_1.z
38
+ .string()
39
+ .describe('Name of the existing table to attach the policies to. Use describe_schema first to verify it exists.'),
40
+ policies: zod_1.z
41
+ .array(PolicyEntrySchema)
42
+ .min(1)
43
+ .describe('One or more policies to add. Each entry becomes one secure_table_provision call.'),
44
+ });
45
+ function buildOperations(roleName, isPermissive) {
46
+ const config = { roleName, isPermissive, policyData: {}, isCustomized: false };
47
+ return { create: config, read: config, update: config, delete: config };
48
+ }
49
+ exports.addPoliciesTool = {
50
+ name: 'add_policies',
51
+ label: 'Add policies',
52
+ description: 'Add one or more RLS policies to an existing table. Has-module policies (AuthzDirectOwner, AuthzEntityMembership, AuthzOrgHierarchy, AuthzPublishable) auto-create the columns they need — call directly, do not add columns first.',
53
+ promptSnippet: 'add_policies: attach RLS policies to an existing table (one call can add many). Gated.',
54
+ parameters: AddPoliciesZod,
55
+ async execute(params, ctx) {
56
+ const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
57
+ if (!resolved.context) {
58
+ return {
59
+ content: [{ type: 'text', text: resolved.reason }],
60
+ details: { success: false, message: resolved.reason },
61
+ };
62
+ }
63
+ const { modules, databaseId, schemaId } = resolved.context;
64
+ try {
65
+ const schema = await (0, schema_resolve_1.resolveSchema)(resolved.context);
66
+ const table = (0, schema_resolve_1.resolveTable)(schema, params.table_name);
67
+ const policies = params.policies.map((entry) => {
68
+ const config = (0, harness_1.getPolicyProvisioningConfig)(entry.policy_type);
69
+ const defaults = (0, harness_1.getPolicyFieldDefaults)(entry.policy_type);
70
+ const mergedData = { ...defaults, ...(entry.data ?? {}) };
71
+ const nodeData = entry.node_data ?? (0, harness_1.buildNodeData)(entry.policy_type, mergedData);
72
+ return {
73
+ policyType: entry.policy_type,
74
+ dataNodeType: config?.dataNodeType,
75
+ nodeData,
76
+ sharedPolicyData: mergedData,
77
+ operations: buildOperations(entry.role_name ?? DEFAULT_ROLE, entry.is_permissive ?? true),
78
+ enabledOperations: entry.operations,
79
+ };
80
+ });
81
+ await (0, add_policies_to_table_1.addPoliciesToExistingTable)({
82
+ client: modules,
83
+ databaseId,
84
+ schemaId,
85
+ tableId: table.id,
86
+ policies,
87
+ });
88
+ const count = params.policies.length;
89
+ const message = `Added ${count} ${count === 1 ? 'policy' : 'policies'} to ${params.table_name}`;
90
+ return { content: [{ type: 'text', text: message }], details: { success: true, message } };
91
+ }
92
+ catch (err) {
93
+ const message = err instanceof Error ? err.message : 'Failed to add policies';
94
+ return { content: [{ type: 'text', text: message }], details: { success: false, message } };
95
+ }
96
+ },
97
+ };
@@ -0,0 +1,18 @@
1
+ import type { HarnessTool } from '@agentic-kit/harness';
2
+ import { z } from 'zod';
3
+ export type AddRecordsDetails = {
4
+ success: boolean;
5
+ message: string;
6
+ tableName?: string;
7
+ columns?: string[];
8
+ rows?: Record<string, unknown>[];
9
+ createdCount?: number;
10
+ asOf?: string;
11
+ needsAuth?: boolean;
12
+ };
13
+ declare const AddRecordsZod: z.ZodObject<{
14
+ table_name: z.ZodString;
15
+ records: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
16
+ }, z.core.$strip>;
17
+ export declare const addRecordsTool: HarnessTool<typeof AddRecordsZod, AddRecordsDetails>;
18
+ export {};
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addRecordsTool = void 0;
4
+ const generators_1 = require("@constructive-io/graphql-query/generators");
5
+ const sdk_1 = require("@constructive-io/sdk");
6
+ const zod_1 = require("zod");
7
+ const context_1 = require("../context");
8
+ const meta_1 = require("../records/meta");
9
+ function fail(message) {
10
+ return { content: [{ type: 'text', text: message }], details: { success: false, message } };
11
+ }
12
+ function needsAuth(message) {
13
+ return {
14
+ content: [{ type: 'text', text: message }],
15
+ details: { success: false, message, needsAuth: true },
16
+ };
17
+ }
18
+ // Mirror @constructive-io/data's prepareCreateInput (create semantics): drop
19
+ // undefined/null/empty-string so server defaults apply; keep false/0/[]/objects.
20
+ function stripForCreate(record) {
21
+ const out = {};
22
+ for (const [key, value] of Object.entries(record)) {
23
+ if (value === undefined || value === null || value === '')
24
+ continue;
25
+ out[key] = value;
26
+ }
27
+ return out;
28
+ }
29
+ const AddRecordsZod = zod_1.z.object({
30
+ table_name: zod_1.z
31
+ .string()
32
+ .describe('Name of the table to insert rows into (snake_case, singular or plural).'),
33
+ records: zod_1.z
34
+ .array(zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()))
35
+ .describe('Rows to insert. Each is an object of column → value (camelCase or snake_case column names).'),
36
+ });
37
+ exports.addRecordsTool = {
38
+ name: 'add_records',
39
+ label: 'Add records',
40
+ description: "Insert one or more rows into a table in the project's app database.",
41
+ promptSnippet: 'add_records: insert rows into an existing table. Gated.',
42
+ parameters: AddRecordsZod,
43
+ async execute(params, ctx) {
44
+ const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
45
+ if (!resolved.context)
46
+ return fail(resolved.reason);
47
+ const { dataEndpoint } = resolved.context;
48
+ if (!dataEndpoint) {
49
+ return fail('Cannot resolve the app data endpoint (DATABASE_NAME missing from .env). Re-provision the database, then retry.');
50
+ }
51
+ if (params.records.length === 0)
52
+ return fail('No records provided.');
53
+ const token = await (0, context_1.resolveDataToken)(resolved.context);
54
+ if (!token.token)
55
+ return needsAuth(token.reason ?? 'Sign in to the database to insert records.');
56
+ const adapter = new sdk_1.objects.FetchAdapter(dataEndpoint, {
57
+ Authorization: `Bearer ${token.token}`,
58
+ });
59
+ let tables;
60
+ try {
61
+ const meta = await adapter.execute(meta_1.META_QUERY);
62
+ if (!meta.ok) {
63
+ return fail(`Failed to read schema from the app database: ${meta.errors.map((e) => e.message).join('; ')}`);
64
+ }
65
+ tables = (meta.data._meta?.tables ?? []).filter((t) => Boolean(t));
66
+ }
67
+ catch (err) {
68
+ return fail(err instanceof Error ? err.message : 'Failed to query the app database schema.');
69
+ }
70
+ const rawTable = (0, meta_1.findMetaTable)(tables, params.table_name);
71
+ if (!rawTable) {
72
+ return fail(`Table "${params.table_name}" was not found in the app database.`);
73
+ }
74
+ const table = (0, meta_1.cleanTable)(rawTable);
75
+ const allTables = tables.map(meta_1.cleanTable);
76
+ const mutation = (0, generators_1.buildPostGraphileCreate)(table, allTables).toString();
77
+ const mutationName = (0, generators_1.toCreateMutationName)(table.name, table);
78
+ const singularName = (0, generators_1.toCamelCaseSingular)(table.name, table);
79
+ // Constructive app tables scope rows by entityId (the owning user's id). The
80
+ // create input requires it, but callers won't know it — backfill it with the
81
+ // signed-in user's id from the Preview session when the table has the column
82
+ // and the record omits it.
83
+ const hasEntityId = table.fields.some((f) => f.name === 'entityId');
84
+ const created = [];
85
+ const errors = [];
86
+ for (const record of params.records) {
87
+ const prepared = hasEntityId && token.userId && record.entityId == null && record.entity_id == null
88
+ ? { ...record, entityId: token.userId }
89
+ : record;
90
+ const variables = { input: { [singularName]: stripForCreate(prepared) } };
91
+ try {
92
+ const result = await adapter.execute(mutation, variables);
93
+ if (!result.ok) {
94
+ errors.push(result.errors.map((e) => e.message).join('; '));
95
+ continue;
96
+ }
97
+ const payload = result.data[mutationName];
98
+ const row = payload?.[singularName] ?? {};
99
+ created.push(row);
100
+ }
101
+ catch (err) {
102
+ errors.push(err instanceof Error ? err.message : 'Unknown error');
103
+ }
104
+ }
105
+ if (created.length === 0) {
106
+ return fail(`Failed to insert into "${params.table_name}": ${errors.join('; ') || 'unknown error'}`);
107
+ }
108
+ const columns = table.fields.map((f) => f.name);
109
+ const message = errors.length > 0
110
+ ? `Inserted ${created.length} of ${params.records.length} row(s) into ${params.table_name} (${errors.length} failed).`
111
+ : `Inserted ${created.length} row(s) into ${params.table_name}.`;
112
+ return {
113
+ content: [{ type: 'text', text: message }],
114
+ details: {
115
+ success: true,
116
+ message,
117
+ tableName: rawTable.name,
118
+ columns,
119
+ rows: created,
120
+ createdCount: created.length,
121
+ asOf: new Date().toISOString(),
122
+ },
123
+ };
124
+ },
125
+ };
@@ -0,0 +1,29 @@
1
+ import type { BlueprintDefinition } from '@agentic-kit/harness';
2
+ import { z } from 'zod';
3
+ export declare const DeleteAction: z.ZodEnum<{
4
+ c: "c";
5
+ r: "r";
6
+ n: "n";
7
+ a: "a";
8
+ d: "d";
9
+ }>;
10
+ export declare const AddRelationZod: z.ZodObject<{
11
+ relation_type: z.ZodOptional<z.ZodEnum<{
12
+ belongs_to: "belongs_to";
13
+ many_to_many: "many_to_many";
14
+ }>>;
15
+ source_table: z.ZodString;
16
+ target_table: z.ZodString;
17
+ field_name: z.ZodOptional<z.ZodString>;
18
+ junction_table_name: z.ZodOptional<z.ZodString>;
19
+ delete_action: z.ZodOptional<z.ZodEnum<{
20
+ c: "c";
21
+ r: "r";
22
+ n: "n";
23
+ a: "a";
24
+ d: "d";
25
+ }>>;
26
+ is_required: z.ZodOptional<z.ZodBoolean>;
27
+ }, z.core.$strip>;
28
+ export type AddRelationParams = z.infer<typeof AddRelationZod>;
29
+ export declare function buildRelation(params: AddRelationParams): BlueprintDefinition['relations'];
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AddRelationZod = exports.DeleteAction = void 0;
4
+ exports.buildRelation = buildRelation;
5
+ const zod_1 = require("zod");
6
+ // delete_action is intentionally always sent: the backend REJECTS a
7
+ // RelationBelongsTo whose delete_action is absent ("PROVISION_RELATION:
8
+ // delete_action is required"). We default it to 'a' (NO ACTION) so the model
9
+ // never has to supply it.
10
+ exports.DeleteAction = zod_1.z
11
+ .enum(['c', 'r', 'n', 'a', 'd'])
12
+ .describe('FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, a=NO ACTION (default), d=SET DEFAULT');
13
+ exports.AddRelationZod = zod_1.z.object({
14
+ relation_type: zod_1.z
15
+ .enum(['belongs_to', 'many_to_many'])
16
+ .describe('belongs_to adds an FK column on the source table; many_to_many adds a junction table. Defaults to belongs_to.')
17
+ .optional(),
18
+ source_table: zod_1.z.string().describe('Existing source table name (snake_case).'),
19
+ target_table: zod_1.z.string().describe('Existing target table name (snake_case).'),
20
+ field_name: zod_1.z
21
+ .string()
22
+ .describe('belongs_to only: FK column name to add on the source table, e.g. "author_id".')
23
+ .optional(),
24
+ junction_table_name: zod_1.z
25
+ .string()
26
+ .describe('many_to_many only: name of the junction table to create, e.g. "post_tags".')
27
+ .optional(),
28
+ delete_action: exports.DeleteAction.optional(),
29
+ is_required: zod_1.z
30
+ .boolean()
31
+ .describe('belongs_to only: make the FK NOT NULL. Defaults to false — a NOT NULL FK fails if the source table already has rows.')
32
+ .optional(),
33
+ });
34
+ // Build a relation-only blueprint definition (no tables): the spike proved
35
+ // constructBlueprint applies relations to PRE-EXISTING tables without recreating
36
+ // them, as long as delete_action is supplied.
37
+ function buildRelation(params) {
38
+ const deleteAction = params.delete_action ?? 'a';
39
+ if ((params.relation_type ?? 'belongs_to') === 'many_to_many') {
40
+ return [
41
+ {
42
+ $type: 'RelationManyToMany',
43
+ source_table: params.source_table,
44
+ target_table: params.target_table,
45
+ junction_table_name: params.junction_table_name,
46
+ delete_action: deleteAction,
47
+ },
48
+ ];
49
+ }
50
+ return [
51
+ {
52
+ $type: 'RelationBelongsTo',
53
+ source_table: params.source_table,
54
+ target_table: params.target_table,
55
+ field_name: params.field_name,
56
+ delete_action: deleteAction,
57
+ is_required: params.is_required ?? false,
58
+ },
59
+ ];
60
+ }
@@ -0,0 +1,10 @@
1
+ import { type HarnessTool } from '@agentic-kit/harness';
2
+ import { AddRelationZod } from './add-relation-schema';
3
+ export type AddRelationDetails = {
4
+ success: boolean;
5
+ message: string;
6
+ sourceTable?: string;
7
+ targetTable?: string;
8
+ fieldName?: string;
9
+ };
10
+ export declare const addRelationTool: HarnessTool<typeof AddRelationZod, AddRelationDetails>;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addRelationTool = void 0;
4
+ const harness_1 = require("@agentic-kit/harness");
5
+ const context_1 = require("../context");
6
+ const schema_resolve_1 = require("../schema-resolve");
7
+ const add_relation_schema_1 = require("./add-relation-schema");
8
+ function fail(message) {
9
+ return { content: [{ type: 'text', text: message }], details: { success: false, message } };
10
+ }
11
+ exports.addRelationTool = {
12
+ name: 'add_relation',
13
+ label: 'Add relation',
14
+ description: 'Add a relation between two EXISTING tables without recreating them: belongs_to adds a foreign-key column on the source table; many_to_many creates a junction table. Use this for relations discovered after the tables were provisioned. For brand-new related tables, prefer a single provision_blueprint call.',
15
+ promptSnippet: 'add_relation: link two existing tables (belongs_to FK column, or many_to_many junction). Gated.',
16
+ parameters: add_relation_schema_1.AddRelationZod,
17
+ async execute(params, ctx) {
18
+ const type = params.relation_type ?? 'belongs_to';
19
+ if (type === 'belongs_to' && !params.field_name) {
20
+ return fail('field_name is required for a belongs_to relation (the FK column name on the source table).');
21
+ }
22
+ if (type === 'many_to_many' && !params.junction_table_name) {
23
+ return fail('junction_table_name is required for a many_to_many relation.');
24
+ }
25
+ const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
26
+ if (!resolved.context)
27
+ return fail(resolved.reason);
28
+ const { modules, databaseId, schemaId, ownerId } = resolved.context;
29
+ if (!ownerId)
30
+ return fail('Not authenticated (missing OWNER_ID in .env).');
31
+ // Validate both tables exist before constructing — a clearer error than the
32
+ // backend's, and avoids a no-op blueprint.
33
+ let schema;
34
+ try {
35
+ schema = await (0, schema_resolve_1.resolveSchema)(resolved.context);
36
+ (0, schema_resolve_1.resolveTable)(schema, params.source_table);
37
+ (0, schema_resolve_1.resolveTable)(schema, params.target_table);
38
+ }
39
+ catch (err) {
40
+ return fail(err instanceof Error ? err.message : 'Failed to resolve schema.');
41
+ }
42
+ const definition = (0, harness_1.expandBlueprintDefaults)({ tables: [], relations: (0, add_relation_schema_1.buildRelation)(params) });
43
+ const uniqueName = `add_relation_${Date.now()}`;
44
+ try {
45
+ const created = await modules.blueprint
46
+ .create({
47
+ data: {
48
+ ownerId,
49
+ databaseId,
50
+ name: uniqueName,
51
+ displayName: `${params.source_table} → ${params.target_table}`,
52
+ description: `Add ${type} relation`,
53
+ definition,
54
+ },
55
+ select: { id: true },
56
+ })
57
+ .unwrap();
58
+ const blueprintId = created.createBlueprint?.blueprint?.id;
59
+ if (!blueprintId)
60
+ throw new Error('Failed to create relation blueprint');
61
+ const constructResult = await modules.mutation
62
+ .constructBlueprint({ input: { blueprintId, schemaId } }, { select: { result: true } })
63
+ .unwrap();
64
+ if (!constructResult.constructBlueprint?.result) {
65
+ const constructions = await modules.blueprintConstruction
66
+ .findMany({
67
+ where: { blueprintId: { equalTo: blueprintId } },
68
+ orderBy: ['ID_DESC'],
69
+ first: 1,
70
+ select: { status: true, errorDetails: true },
71
+ })
72
+ .unwrap();
73
+ const error = constructions?.blueprintConstructions?.nodes?.[0]?.errorDetails ?? 'Unknown error';
74
+ return fail(`Failed to add relation: ${typeof error === 'string' ? error : JSON.stringify(error)}`);
75
+ }
76
+ }
77
+ catch (err) {
78
+ return fail(`Failed to add relation: ${err instanceof Error ? err.message : String(err)}`);
79
+ }
80
+ const message = type === 'many_to_many'
81
+ ? `Created junction table "${params.junction_table_name}" linking ${params.source_table} ↔ ${params.target_table}.`
82
+ : `Added FK "${params.field_name}" on ${params.source_table} → ${params.target_table}.`;
83
+ return {
84
+ content: [{ type: 'text', text: message }],
85
+ details: {
86
+ success: true,
87
+ message,
88
+ sourceTable: params.source_table,
89
+ targetTable: params.target_table,
90
+ fieldName: params.field_name,
91
+ },
92
+ };
93
+ },
94
+ };
@@ -0,0 +1,25 @@
1
+ import type { HarnessTool } from '@agentic-kit/harness';
2
+ import { z } from 'zod';
3
+ declare const CreateApiKeyZod: z.ZodObject<{
4
+ key_name: z.ZodString;
5
+ principal_name: z.ZodOptional<z.ZodString>;
6
+ entity_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ read_only: z.ZodOptional<z.ZodBoolean>;
8
+ expires_in_days: z.ZodOptional<z.ZodNumber>;
9
+ }, z.core.$strip>;
10
+ export type CreateApiKeyDetails = {
11
+ success: boolean;
12
+ message: string;
13
+ keyId?: string;
14
+ envVar?: string;
15
+ expiresAt?: string;
16
+ principalId?: string;
17
+ principalName?: string;
18
+ scope?: string;
19
+ needsAuth?: boolean;
20
+ };
21
+ export declare function toEnvVar(keyName: string): string;
22
+ export declare function isStepUpError(messages: string[]): boolean;
23
+ export declare function describeScope(entityIds: string[] | undefined, readOnly: boolean): string;
24
+ export declare const createApiKeyTool: HarnessTool<typeof CreateApiKeyZod, CreateApiKeyDetails>;
25
+ export {};