@facilis.dev/db 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +18 -2
- package/dist/client.d.ts +78 -0
- package/dist/client.js +208 -0
- package/dist/files.d.ts +268 -0
- package/dist/files.js +88 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/jobs.d.ts +255 -0
- package/dist/jobs.js +64 -0
- package/dist/live-schema.d.ts +39 -0
- package/dist/live-schema.js +97 -0
- package/dist/migrator.d.ts +20 -0
- package/dist/migrator.js +66 -0
- package/dist/policy.d.ts +50 -0
- package/dist/policy.js +111 -0
- package/dist/query-console.d.ts +44 -0
- package/dist/query-console.js +173 -0
- package/dist/read-only-sql.d.ts +13 -0
- package/dist/read-only-sql.js +145 -0
- package/dist/rls.d.ts +37 -0
- package/dist/rls.js +90 -0
- package/dist/schema-digest.d.ts +97 -0
- package/dist/schema-digest.js +178 -0
- package/dist/schema-drift.d.ts +43 -0
- package/dist/schema-drift.js +176 -0
- package/dist/snapshot-readers.d.ts +3 -0
- package/dist/snapshot-readers.js +182 -0
- package/dist/snapshot.d.ts +92 -0
- package/dist/snapshot.js +21 -0
- package/dist/sudo-audit.d.ts +60 -0
- package/dist/sudo-audit.js +45 -0
- package/dist/sudo.d.ts +54 -0
- package/dist/sudo.js +137 -0
- package/dist/table-guard.d.ts +37 -0
- package/dist/table-guard.js +228 -0
- package/dist/tasks.d.ts +312 -0
- package/dist/tasks.js +52 -0
- package/dist/tenant-column.d.ts +21 -0
- package/dist/tenant-column.js +27 -0
- package/dist/views.d.ts +92 -0
- package/dist/views.js +186 -0
- package/dist/workflows.d.ts +289 -0
- package/dist/workflows.js +55 -0
- package/package.json +43 -4
- package/src/client.ts +291 -0
- package/src/files.ts +124 -0
- package/src/index.ts +174 -0
- package/src/jobs.ts +76 -0
- package/src/live-schema.ts +145 -0
- package/src/migrator.ts +92 -0
- package/src/policy.ts +174 -0
- package/src/query-console.ts +215 -0
- package/src/read-only-sql.ts +156 -0
- package/src/rls.ts +109 -0
- package/src/schema-digest.ts +240 -0
- package/src/schema-drift.ts +234 -0
- package/src/snapshot-readers.ts +212 -0
- package/src/snapshot.ts +122 -0
- package/src/sudo-audit.ts +91 -0
- package/src/sudo.ts +200 -0
- package/src/table-guard.ts +296 -0
- package/src/tasks.ts +79 -0
- package/src/tenant-column.ts +29 -0
- package/src/views.ts +291 -0
- package/src/workflows.ts +76 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @facilis.dev/db — Drizzle client for PGlite and Postgres, policy scope, RLS policy
|
|
3
|
+
* generators, schema snapshot and idempotent statement runner.
|
|
4
|
+
*/
|
|
5
|
+
export { createDb, DbConnectionError, DEFAULT_OWNER_COLUMNS, DEFAULT_TENANT_COLUMNS, DriverNotImplementedError, SnapshotUnavailableError, type CreateDbOptions, type DbDriver, type FacilisDb, type FacilisSchema, type PgDatabaseLike, } from "./client.js";
|
|
6
|
+
export { APP_ROLE, AppRoleUnavailableError, ensureAppRole, runWithPolicyScope, type AppRoleProvisioning, type PolicyScope, type PolicyScopedDatabase, type PolicyScopedTransaction, } from "./policy.js";
|
|
7
|
+
export { ownerIsolationPolicy, policyCreateSql, rlsForTable, rolePolicy, tenantIsolationPolicy, withRls, type RlsExtras, type RlsOptions, } from "./rls.js";
|
|
8
|
+
export { tenantColumn, tenantDefaultSql } from "./tenant-column.js";
|
|
9
|
+
export { facilisFiles, fileLinks, type FacilisFile, type FileLinksOptions, type FileLinksTable, type NewFacilisFile, } from "./files.js";
|
|
10
|
+
export { schemaSnapshot, type CheckSnapshot, type ColumnSnapshot, type EnumSnapshot, type ForeignKeySnapshot, type IndexSnapshot, type PolicySnapshot, type SchemaSnapshot, type TableSnapshot, type UniqueConstraintSnapshot, type UnsupportedDeclarationSnapshot, } from "./snapshot.js";
|
|
11
|
+
export { liveSchemaSnapshot, type LiveColumnSnapshot, type LivePolicySnapshot, type LiveSchemaSnapshot, type LiveTableSnapshot, } from "./live-schema.js";
|
|
12
|
+
export { canonicalColumnType, diffSchemaSnapshots, type SchemaDrift, type SchemaDriftKind, } from "./schema-drift.js";
|
|
13
|
+
export { canonicalDigestJson, declaredDigestSchema, digestSchema, liveDigestSchema, SCHEMA_DIGEST_VERSION, type DigestColumn, type DigestPolicy, type DigestSchema, type DigestTable, } from "./schema-digest.js";
|
|
14
|
+
export { applyMigrations, applyStatements, type MigrationSource } from "./migrator.js";
|
|
15
|
+
export { applyViews, collectViewMetadata, defineView, grantViewSelect, isFacilisView, viewCreateStatement, viewDefinitionStatement, viewMetadata, ViewDefinitionError, VIEW_BRAND, VIEW_NAME_PATTERN, VIEW_PERMISSION_PATTERN, type DefineViewOptions, type FacilisView, type ViewApplyFailure, type ViewApplyOptions, type ViewApplyResult, type ViewErrorCode, type ViewMetadata, } from "./views.js";
|
|
16
|
+
export { ensureSystemRole, runAsSystem, runWithSystemRole, SUDO_ID_GUC, SYSTEM_ROLE, type RunAsSystemOptions, type RunWithSystemRoleOptions, } from "./sudo.js";
|
|
17
|
+
export { FACILIS_JOB_STATUSES, facilisJobs, type FacilisJobRow, type FacilisJobStatus, type NewFacilisJobRow, } from "./jobs.js";
|
|
18
|
+
export { FACILIS_TASK_DECISIONS, FACILIS_TASK_KINDS, FACILIS_TASK_STATUSES, facilisTasks, type FacilisTaskDecision, type FacilisTaskKind, type FacilisTaskRow, type FacilisTaskStatus, type NewFacilisTaskRow, } from "./tasks.js";
|
|
19
|
+
export { FACILIS_WORKFLOW_STATUSES, facilisWorkflows, type FacilisWorkflowRow, type FacilisWorkflowStatus, type NewFacilisWorkflowRow, } from "./workflows.js";
|
|
20
|
+
export { newSudoId, resetSudoAuditSink, setSudoAuditSink, writeSudoAudit, SudoAuditUnavailableError, type SudoAuditEvent, type SudoAuditEventName, type SudoAuditSink, } from "./sudo-audit.js";
|
|
21
|
+
export { firstKeyword, hasMultipleStatements, isReadOnlySql, stripTrailingSemicolon, } from "./read-only-sql.js";
|
|
22
|
+
export { guardDatabase, TableAccessError, type TableAccessErrorCode, type TableGuardOptions, } from "./table-guard.js";
|
|
23
|
+
export { createLiveView, DEFAULT_QUERY_LIMIT, MAX_QUERY_LIMIT, QueryExecutionError, QUERY_STATEMENT_TIMEOUT_MS, runReadOnlyQuery, toJsonValue, type QueryColumn, type QueryExecution, type QueryExecutionErrorCode, } from "./query-console.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @facilis.dev/db — Drizzle client for PGlite and Postgres, policy scope, RLS policy
|
|
3
|
+
* generators, schema snapshot and idempotent statement runner.
|
|
4
|
+
*/
|
|
5
|
+
export { createDb, DbConnectionError, DEFAULT_OWNER_COLUMNS, DEFAULT_TENANT_COLUMNS, DriverNotImplementedError, SnapshotUnavailableError, } from "./client.js";
|
|
6
|
+
export { APP_ROLE, AppRoleUnavailableError, ensureAppRole, runWithPolicyScope, } from "./policy.js";
|
|
7
|
+
export { ownerIsolationPolicy, policyCreateSql, rlsForTable, rolePolicy, tenantIsolationPolicy, withRls, } from "./rls.js";
|
|
8
|
+
export { tenantColumn, tenantDefaultSql } from "./tenant-column.js";
|
|
9
|
+
export { facilisFiles, fileLinks, } from "./files.js";
|
|
10
|
+
export { schemaSnapshot, } from "./snapshot.js";
|
|
11
|
+
export { liveSchemaSnapshot, } from "./live-schema.js";
|
|
12
|
+
export { canonicalColumnType, diffSchemaSnapshots, } from "./schema-drift.js";
|
|
13
|
+
export { canonicalDigestJson, declaredDigestSchema, digestSchema, liveDigestSchema, SCHEMA_DIGEST_VERSION, } from "./schema-digest.js";
|
|
14
|
+
export { applyMigrations, applyStatements } from "./migrator.js";
|
|
15
|
+
export { applyViews, collectViewMetadata, defineView, grantViewSelect, isFacilisView, viewCreateStatement, viewDefinitionStatement, viewMetadata, ViewDefinitionError, VIEW_BRAND, VIEW_NAME_PATTERN, VIEW_PERMISSION_PATTERN, } from "./views.js";
|
|
16
|
+
export { ensureSystemRole, runAsSystem, runWithSystemRole, SUDO_ID_GUC, SYSTEM_ROLE, } from "./sudo.js";
|
|
17
|
+
export { FACILIS_JOB_STATUSES, facilisJobs, } from "./jobs.js";
|
|
18
|
+
export { FACILIS_TASK_DECISIONS, FACILIS_TASK_KINDS, FACILIS_TASK_STATUSES, facilisTasks, } from "./tasks.js";
|
|
19
|
+
export { FACILIS_WORKFLOW_STATUSES, facilisWorkflows, } from "./workflows.js";
|
|
20
|
+
export { newSudoId, resetSudoAuditSink, setSudoAuditSink, writeSudoAudit, SudoAuditUnavailableError, } from "./sudo-audit.js";
|
|
21
|
+
export { firstKeyword, hasMultipleStatements, isReadOnlySql, stripTrailingSemicolon, } from "./read-only-sql.js";
|
|
22
|
+
export { guardDatabase, TableAccessError, } from "./table-guard.js";
|
|
23
|
+
export { createLiveView, DEFAULT_QUERY_LIMIT, MAX_QUERY_LIMIT, QueryExecutionError, QUERY_STATEMENT_TIMEOUT_MS, runReadOnlyQuery, toJsonValue, } from "./query-console.js";
|
package/dist/jobs.d.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/** Every `facilis_jobs.status` value, in lifecycle order. */
|
|
2
|
+
export declare const FACILIS_JOB_STATUSES: readonly ["queued", "running", "succeeded", "failed", "cancelled"];
|
|
3
|
+
export type FacilisJobStatus = (typeof FACILIS_JOB_STATUSES)[number];
|
|
4
|
+
/**
|
|
5
|
+
* One job instance. `input`/`output` are JSONB because the job contract is serialized
|
|
6
|
+
* data (docs/JOBS.md §5); `status` is text rather than a Postgres enum so a new status is a
|
|
7
|
+
* code change, not a migration.
|
|
8
|
+
*/
|
|
9
|
+
export declare const facilisJobs: Omit<import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
10
|
+
name: "facilis_jobs";
|
|
11
|
+
schema: undefined;
|
|
12
|
+
columns: {
|
|
13
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
14
|
+
name: "id";
|
|
15
|
+
tableName: "facilis_jobs";
|
|
16
|
+
dataType: "string";
|
|
17
|
+
columnType: "PgUUID";
|
|
18
|
+
data: string;
|
|
19
|
+
driverParam: string;
|
|
20
|
+
notNull: true;
|
|
21
|
+
hasDefault: true;
|
|
22
|
+
isPrimaryKey: true;
|
|
23
|
+
isAutoincrement: false;
|
|
24
|
+
hasRuntimeDefault: false;
|
|
25
|
+
enumValues: undefined;
|
|
26
|
+
baseColumn: never;
|
|
27
|
+
identity: undefined;
|
|
28
|
+
generated: undefined;
|
|
29
|
+
}, {}, {}>;
|
|
30
|
+
name: import("drizzle-orm/pg-core").PgColumn<{
|
|
31
|
+
name: "name";
|
|
32
|
+
tableName: "facilis_jobs";
|
|
33
|
+
dataType: "string";
|
|
34
|
+
columnType: "PgText";
|
|
35
|
+
data: string;
|
|
36
|
+
driverParam: string;
|
|
37
|
+
notNull: true;
|
|
38
|
+
hasDefault: false;
|
|
39
|
+
isPrimaryKey: false;
|
|
40
|
+
isAutoincrement: false;
|
|
41
|
+
hasRuntimeDefault: false;
|
|
42
|
+
enumValues: [string, ...string[]];
|
|
43
|
+
baseColumn: never;
|
|
44
|
+
identity: undefined;
|
|
45
|
+
generated: undefined;
|
|
46
|
+
}, {}, {}>;
|
|
47
|
+
status: import("drizzle-orm/pg-core").PgColumn<{
|
|
48
|
+
name: "status";
|
|
49
|
+
tableName: "facilis_jobs";
|
|
50
|
+
dataType: "string";
|
|
51
|
+
columnType: "PgText";
|
|
52
|
+
data: string;
|
|
53
|
+
driverParam: string;
|
|
54
|
+
notNull: true;
|
|
55
|
+
hasDefault: false;
|
|
56
|
+
isPrimaryKey: false;
|
|
57
|
+
isAutoincrement: false;
|
|
58
|
+
hasRuntimeDefault: false;
|
|
59
|
+
enumValues: [string, ...string[]];
|
|
60
|
+
baseColumn: never;
|
|
61
|
+
identity: undefined;
|
|
62
|
+
generated: undefined;
|
|
63
|
+
}, {}, {}>;
|
|
64
|
+
input: import("drizzle-orm/pg-core").PgColumn<{
|
|
65
|
+
name: "input";
|
|
66
|
+
tableName: "facilis_jobs";
|
|
67
|
+
dataType: "json";
|
|
68
|
+
columnType: "PgJsonb";
|
|
69
|
+
data: unknown;
|
|
70
|
+
driverParam: unknown;
|
|
71
|
+
notNull: true;
|
|
72
|
+
hasDefault: false;
|
|
73
|
+
isPrimaryKey: false;
|
|
74
|
+
isAutoincrement: false;
|
|
75
|
+
hasRuntimeDefault: false;
|
|
76
|
+
enumValues: undefined;
|
|
77
|
+
baseColumn: never;
|
|
78
|
+
identity: undefined;
|
|
79
|
+
generated: undefined;
|
|
80
|
+
}, {}, {}>;
|
|
81
|
+
output: import("drizzle-orm/pg-core").PgColumn<{
|
|
82
|
+
name: "output";
|
|
83
|
+
tableName: "facilis_jobs";
|
|
84
|
+
dataType: "json";
|
|
85
|
+
columnType: "PgJsonb";
|
|
86
|
+
data: unknown;
|
|
87
|
+
driverParam: unknown;
|
|
88
|
+
notNull: false;
|
|
89
|
+
hasDefault: false;
|
|
90
|
+
isPrimaryKey: false;
|
|
91
|
+
isAutoincrement: false;
|
|
92
|
+
hasRuntimeDefault: false;
|
|
93
|
+
enumValues: undefined;
|
|
94
|
+
baseColumn: never;
|
|
95
|
+
identity: undefined;
|
|
96
|
+
generated: undefined;
|
|
97
|
+
}, {}, {}>;
|
|
98
|
+
error: import("drizzle-orm/pg-core").PgColumn<{
|
|
99
|
+
name: "error";
|
|
100
|
+
tableName: "facilis_jobs";
|
|
101
|
+
dataType: "string";
|
|
102
|
+
columnType: "PgText";
|
|
103
|
+
data: string;
|
|
104
|
+
driverParam: string;
|
|
105
|
+
notNull: false;
|
|
106
|
+
hasDefault: false;
|
|
107
|
+
isPrimaryKey: false;
|
|
108
|
+
isAutoincrement: false;
|
|
109
|
+
hasRuntimeDefault: false;
|
|
110
|
+
enumValues: [string, ...string[]];
|
|
111
|
+
baseColumn: never;
|
|
112
|
+
identity: undefined;
|
|
113
|
+
generated: undefined;
|
|
114
|
+
}, {}, {}>;
|
|
115
|
+
attempts: import("drizzle-orm/pg-core").PgColumn<{
|
|
116
|
+
name: "attempts";
|
|
117
|
+
tableName: "facilis_jobs";
|
|
118
|
+
dataType: "number";
|
|
119
|
+
columnType: "PgInteger";
|
|
120
|
+
data: number;
|
|
121
|
+
driverParam: string | number;
|
|
122
|
+
notNull: true;
|
|
123
|
+
hasDefault: true;
|
|
124
|
+
isPrimaryKey: false;
|
|
125
|
+
isAutoincrement: false;
|
|
126
|
+
hasRuntimeDefault: false;
|
|
127
|
+
enumValues: undefined;
|
|
128
|
+
baseColumn: never;
|
|
129
|
+
identity: undefined;
|
|
130
|
+
generated: undefined;
|
|
131
|
+
}, {}, {}>;
|
|
132
|
+
maxAttempts: import("drizzle-orm/pg-core").PgColumn<{
|
|
133
|
+
name: "max_attempts";
|
|
134
|
+
tableName: "facilis_jobs";
|
|
135
|
+
dataType: "number";
|
|
136
|
+
columnType: "PgInteger";
|
|
137
|
+
data: number;
|
|
138
|
+
driverParam: string | number;
|
|
139
|
+
notNull: true;
|
|
140
|
+
hasDefault: false;
|
|
141
|
+
isPrimaryKey: false;
|
|
142
|
+
isAutoincrement: false;
|
|
143
|
+
hasRuntimeDefault: false;
|
|
144
|
+
enumValues: undefined;
|
|
145
|
+
baseColumn: never;
|
|
146
|
+
identity: undefined;
|
|
147
|
+
generated: undefined;
|
|
148
|
+
}, {}, {}>;
|
|
149
|
+
timeoutMs: import("drizzle-orm/pg-core").PgColumn<{
|
|
150
|
+
name: "timeout_ms";
|
|
151
|
+
tableName: "facilis_jobs";
|
|
152
|
+
dataType: "number";
|
|
153
|
+
columnType: "PgInteger";
|
|
154
|
+
data: number;
|
|
155
|
+
driverParam: string | number;
|
|
156
|
+
notNull: true;
|
|
157
|
+
hasDefault: false;
|
|
158
|
+
isPrimaryKey: false;
|
|
159
|
+
isAutoincrement: false;
|
|
160
|
+
hasRuntimeDefault: false;
|
|
161
|
+
enumValues: undefined;
|
|
162
|
+
baseColumn: never;
|
|
163
|
+
identity: undefined;
|
|
164
|
+
generated: undefined;
|
|
165
|
+
}, {}, {}>;
|
|
166
|
+
runAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
167
|
+
name: "run_at";
|
|
168
|
+
tableName: "facilis_jobs";
|
|
169
|
+
dataType: "date";
|
|
170
|
+
columnType: "PgTimestamp";
|
|
171
|
+
data: Date;
|
|
172
|
+
driverParam: string;
|
|
173
|
+
notNull: true;
|
|
174
|
+
hasDefault: true;
|
|
175
|
+
isPrimaryKey: false;
|
|
176
|
+
isAutoincrement: false;
|
|
177
|
+
hasRuntimeDefault: false;
|
|
178
|
+
enumValues: undefined;
|
|
179
|
+
baseColumn: never;
|
|
180
|
+
identity: undefined;
|
|
181
|
+
generated: undefined;
|
|
182
|
+
}, {}, {}>;
|
|
183
|
+
startedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
184
|
+
name: "started_at";
|
|
185
|
+
tableName: "facilis_jobs";
|
|
186
|
+
dataType: "date";
|
|
187
|
+
columnType: "PgTimestamp";
|
|
188
|
+
data: Date;
|
|
189
|
+
driverParam: string;
|
|
190
|
+
notNull: false;
|
|
191
|
+
hasDefault: false;
|
|
192
|
+
isPrimaryKey: false;
|
|
193
|
+
isAutoincrement: false;
|
|
194
|
+
hasRuntimeDefault: false;
|
|
195
|
+
enumValues: undefined;
|
|
196
|
+
baseColumn: never;
|
|
197
|
+
identity: undefined;
|
|
198
|
+
generated: undefined;
|
|
199
|
+
}, {}, {}>;
|
|
200
|
+
finishedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
201
|
+
name: "finished_at";
|
|
202
|
+
tableName: "facilis_jobs";
|
|
203
|
+
dataType: "date";
|
|
204
|
+
columnType: "PgTimestamp";
|
|
205
|
+
data: Date;
|
|
206
|
+
driverParam: string;
|
|
207
|
+
notNull: false;
|
|
208
|
+
hasDefault: false;
|
|
209
|
+
isPrimaryKey: false;
|
|
210
|
+
isAutoincrement: false;
|
|
211
|
+
hasRuntimeDefault: false;
|
|
212
|
+
enumValues: undefined;
|
|
213
|
+
baseColumn: never;
|
|
214
|
+
identity: undefined;
|
|
215
|
+
generated: undefined;
|
|
216
|
+
}, {}, {}>;
|
|
217
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
218
|
+
name: "created_at";
|
|
219
|
+
tableName: "facilis_jobs";
|
|
220
|
+
dataType: "date";
|
|
221
|
+
columnType: "PgTimestamp";
|
|
222
|
+
data: Date;
|
|
223
|
+
driverParam: string;
|
|
224
|
+
notNull: true;
|
|
225
|
+
hasDefault: true;
|
|
226
|
+
isPrimaryKey: false;
|
|
227
|
+
isAutoincrement: false;
|
|
228
|
+
hasRuntimeDefault: false;
|
|
229
|
+
enumValues: undefined;
|
|
230
|
+
baseColumn: never;
|
|
231
|
+
identity: undefined;
|
|
232
|
+
generated: undefined;
|
|
233
|
+
}, {}, {}>;
|
|
234
|
+
tenantId: import("drizzle-orm/pg-core").PgColumn<{
|
|
235
|
+
name: string;
|
|
236
|
+
tableName: "facilis_jobs";
|
|
237
|
+
dataType: "string";
|
|
238
|
+
columnType: "PgUUID";
|
|
239
|
+
data: string;
|
|
240
|
+
driverParam: string;
|
|
241
|
+
notNull: true;
|
|
242
|
+
hasDefault: true;
|
|
243
|
+
isPrimaryKey: false;
|
|
244
|
+
isAutoincrement: false;
|
|
245
|
+
hasRuntimeDefault: false;
|
|
246
|
+
enumValues: undefined;
|
|
247
|
+
baseColumn: never;
|
|
248
|
+
identity: undefined;
|
|
249
|
+
generated: undefined;
|
|
250
|
+
}, {}, {}>;
|
|
251
|
+
};
|
|
252
|
+
dialect: 'pg';
|
|
253
|
+
}>, "enableRLS">;
|
|
254
|
+
export type FacilisJobRow = typeof facilisJobs.$inferSelect;
|
|
255
|
+
export type NewFacilisJobRow = typeof facilisJobs.$inferInsert;
|
package/dist/jobs.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The persisted job queue (D-107, docs/JOBS.md §8).
|
|
2
|
+
//
|
|
3
|
+
// The queue is framework state, not application data: it is tenant-scoped so the structural
|
|
4
|
+
// invariant holds (`pgPolicy` + `ENABLE ROW LEVEL SECURITY`), the project re-exports the
|
|
5
|
+
// table from `db/schema.ts`, and the framework owns its DDL via a Drizzle migration. The
|
|
6
|
+
// engine's own writes run as the app role under the deployment tenant; a job *body* runs as
|
|
7
|
+
// the system role in its own audited transaction.
|
|
8
|
+
import { sql } from "drizzle-orm";
|
|
9
|
+
import { integer, jsonb, pgPolicy, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
10
|
+
import { tenantIsolationPolicy } from "./rls.js";
|
|
11
|
+
import { tenantColumn } from "./tenant-column.js";
|
|
12
|
+
/** Every `facilis_jobs.status` value, in lifecycle order. */
|
|
13
|
+
export const FACILIS_JOB_STATUSES = [
|
|
14
|
+
"queued",
|
|
15
|
+
"running",
|
|
16
|
+
"succeeded",
|
|
17
|
+
"failed",
|
|
18
|
+
"cancelled",
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* One job instance. `input`/`output` are JSONB because the job contract is serialized
|
|
22
|
+
* data (docs/JOBS.md §5); `status` is text rather than a Postgres enum so a new status is a
|
|
23
|
+
* code change, not a migration.
|
|
24
|
+
*/
|
|
25
|
+
export const facilisJobs = pgTable("facilis_jobs", {
|
|
26
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
27
|
+
name: text("name").notNull(),
|
|
28
|
+
status: text("status").notNull(),
|
|
29
|
+
input: jsonb("input").notNull(),
|
|
30
|
+
output: jsonb("output"),
|
|
31
|
+
error: text("error"),
|
|
32
|
+
attempts: integer("attempts").notNull().default(0),
|
|
33
|
+
maxAttempts: integer("max_attempts").notNull(),
|
|
34
|
+
timeoutMs: integer("timeout_ms").notNull(),
|
|
35
|
+
/** When the job becomes eligible (a future instant means "scheduled"). */
|
|
36
|
+
runAt: timestamp("run_at", { withTimezone: true }).notNull().defaultNow(),
|
|
37
|
+
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
38
|
+
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
|
39
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
40
|
+
tenantId: tenantColumn(),
|
|
41
|
+
}, () => [
|
|
42
|
+
tenantIsolationPolicy(),
|
|
43
|
+
// RLS is tenant-only (D-008); the queue is additionally gated by app permissions, so a
|
|
44
|
+
// route that touches facilis_jobs without jobs:read/jobs:exec fails at the database, not
|
|
45
|
+
// only in its own wrapper check. The framework engine's scope passes both permissions
|
|
46
|
+
// explicitly (db-store); a request scope carries the session's expanded permissions.
|
|
47
|
+
pgPolicy("facilis_jobs_read", {
|
|
48
|
+
for: "select",
|
|
49
|
+
using: sql `nullif(current_setting('app.permissions', true), '')::jsonb ? 'jobs:read'`,
|
|
50
|
+
}),
|
|
51
|
+
pgPolicy("facilis_jobs_insert", {
|
|
52
|
+
for: "insert",
|
|
53
|
+
withCheck: sql `nullif(current_setting('app.permissions', true), '')::jsonb ? 'jobs:exec'`,
|
|
54
|
+
}),
|
|
55
|
+
pgPolicy("facilis_jobs_update", {
|
|
56
|
+
for: "update",
|
|
57
|
+
using: sql `nullif(current_setting('app.permissions', true), '')::jsonb ? 'jobs:exec'`,
|
|
58
|
+
withCheck: sql `nullif(current_setting('app.permissions', true), '')::jsonb ? 'jobs:exec'`,
|
|
59
|
+
}),
|
|
60
|
+
pgPolicy("facilis_jobs_delete", {
|
|
61
|
+
for: "delete",
|
|
62
|
+
using: sql `nullif(current_setting('app.permissions', true), '')::jsonb ? 'jobs:exec'`,
|
|
63
|
+
}),
|
|
64
|
+
]).enableRLS();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FacilisDb } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Live database introspection (SPEC §11.1): the catalog side of the code↔live diff.
|
|
4
|
+
*
|
|
5
|
+
* Everything here reads Postgres system catalogs, never the Drizzle schema, so the
|
|
6
|
+
* snapshot describes the database the running project is actually talking to (PGlite in
|
|
7
|
+
* dev, Postgres in production). Tables are restricted to the connection's current schema:
|
|
8
|
+
* Facilis projects keep their tables in `public`, and framework-internal schemas such as
|
|
9
|
+
* `drizzle` must not show up as drift.
|
|
10
|
+
*/
|
|
11
|
+
export interface LiveColumnSnapshot {
|
|
12
|
+
name: string;
|
|
13
|
+
/** `format_type()` rendering, e.g. `character varying(120)` or `timestamp with time zone`. */
|
|
14
|
+
type: string;
|
|
15
|
+
notNull: boolean;
|
|
16
|
+
default: string | null;
|
|
17
|
+
isPrimaryKey: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface LivePolicySnapshot {
|
|
20
|
+
name: string;
|
|
21
|
+
/** SQL command the policy applies to: ALL, SELECT, INSERT, UPDATE or DELETE. */
|
|
22
|
+
command: string;
|
|
23
|
+
/** RESTRICTIVE policies are AND-ed with the permissive ones (D-008). */
|
|
24
|
+
restrictive: boolean;
|
|
25
|
+
roles: string[];
|
|
26
|
+
using: string | null;
|
|
27
|
+
withCheck: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface LiveTableSnapshot {
|
|
30
|
+
name: string;
|
|
31
|
+
rlsEnabled: boolean;
|
|
32
|
+
columns: LiveColumnSnapshot[];
|
|
33
|
+
policies: LivePolicySnapshot[];
|
|
34
|
+
}
|
|
35
|
+
export interface LiveSchemaSnapshot {
|
|
36
|
+
tables: LiveTableSnapshot[];
|
|
37
|
+
}
|
|
38
|
+
/** Introspect the live database behind a Facilis project. */
|
|
39
|
+
export declare function liveSchemaSnapshot(db: FacilisDb): Promise<LiveSchemaSnapshot>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { MIGRATION_TRACKING_TABLE } from "./migrator.js";
|
|
3
|
+
/**
|
|
4
|
+
* Tables this package owns for its own bookkeeping. They exist in every migrated database
|
|
5
|
+
* but never in the project schema, so they would otherwise show up as permanent drift.
|
|
6
|
+
*/
|
|
7
|
+
const INTERNAL_TABLES = new Set([MIGRATION_TRACKING_TABLE]);
|
|
8
|
+
/**
|
|
9
|
+
* One row per column; table-level attributes (`relrowsecurity`) repeat on every row and
|
|
10
|
+
* are lifted onto the table when the first column is seen.
|
|
11
|
+
*/
|
|
12
|
+
const COLUMNS_QUERY = sql `
|
|
13
|
+
select
|
|
14
|
+
c.relname as table_name,
|
|
15
|
+
c.relrowsecurity as rls_enabled,
|
|
16
|
+
a.attname as column_name,
|
|
17
|
+
format_type(a.atttypid, a.atttypmod) as column_type,
|
|
18
|
+
a.attnotnull as not_null,
|
|
19
|
+
pg_get_expr(d.adbin, d.adrelid) as column_default,
|
|
20
|
+
exists (
|
|
21
|
+
select 1
|
|
22
|
+
from pg_index i
|
|
23
|
+
where i.indrelid = c.oid and i.indisprimary and a.attnum = any (i.indkey)
|
|
24
|
+
) as is_primary_key
|
|
25
|
+
from pg_class c
|
|
26
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
27
|
+
join pg_attribute a on a.attrelid = c.oid
|
|
28
|
+
left join pg_attrdef d on d.adrelid = c.oid and d.adnum = a.attnum
|
|
29
|
+
where n.nspname = current_schema()
|
|
30
|
+
and c.relkind = 'r'
|
|
31
|
+
and a.attnum > 0
|
|
32
|
+
and not a.attisdropped
|
|
33
|
+
order by c.relname, a.attnum
|
|
34
|
+
`;
|
|
35
|
+
/** `pg_policies` is the catalog view over `pg_policy`; it renders each qualifier as SQL. */
|
|
36
|
+
const POLICIES_QUERY = sql `
|
|
37
|
+
select tablename, policyname, permissive, roles, cmd, qual, with_check
|
|
38
|
+
from pg_policies
|
|
39
|
+
where schemaname = current_schema()
|
|
40
|
+
order by tablename, policyname
|
|
41
|
+
`;
|
|
42
|
+
/** Introspect the live database behind a Facilis project. */
|
|
43
|
+
export async function liveSchemaSnapshot(db) {
|
|
44
|
+
const columnResult = await db.db.execute(COLUMNS_QUERY);
|
|
45
|
+
const tables = new Map();
|
|
46
|
+
for (const row of columnResult.rows) {
|
|
47
|
+
const tableName = readText(row, "table_name");
|
|
48
|
+
const columnName = readText(row, "column_name");
|
|
49
|
+
if (tableName === null || columnName === null || INTERNAL_TABLES.has(tableName))
|
|
50
|
+
continue;
|
|
51
|
+
const table = tableFor(tables, tableName);
|
|
52
|
+
table.rlsEnabled = row["rls_enabled"] === true;
|
|
53
|
+
table.columns.push({
|
|
54
|
+
name: columnName,
|
|
55
|
+
type: readText(row, "column_type") ?? "",
|
|
56
|
+
notNull: row["not_null"] === true,
|
|
57
|
+
default: readText(row, "column_default"),
|
|
58
|
+
isPrimaryKey: row["is_primary_key"] === true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const policyResult = await db.db.execute(POLICIES_QUERY);
|
|
62
|
+
for (const row of policyResult.rows) {
|
|
63
|
+
const tableName = readText(row, "tablename");
|
|
64
|
+
const policyName = readText(row, "policyname");
|
|
65
|
+
if (tableName === null || policyName === null || INTERNAL_TABLES.has(tableName))
|
|
66
|
+
continue;
|
|
67
|
+
tableFor(tables, tableName).policies.push({
|
|
68
|
+
name: policyName,
|
|
69
|
+
command: (readText(row, "cmd") ?? "ALL").toUpperCase(),
|
|
70
|
+
restrictive: (readText(row, "permissive") ?? "").toUpperCase() === "RESTRICTIVE",
|
|
71
|
+
roles: readTextArray(row, "roles"),
|
|
72
|
+
using: readText(row, "qual"),
|
|
73
|
+
withCheck: readText(row, "with_check"),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
tables: [...tables.values()].toSorted((left, right) => left.name.localeCompare(right.name)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function tableFor(tables, name) {
|
|
81
|
+
const existing = tables.get(name);
|
|
82
|
+
if (existing !== undefined)
|
|
83
|
+
return existing;
|
|
84
|
+
const table = { name, rlsEnabled: false, columns: [], policies: [] };
|
|
85
|
+
tables.set(name, table);
|
|
86
|
+
return table;
|
|
87
|
+
}
|
|
88
|
+
function readText(row, key) {
|
|
89
|
+
const value = row[key];
|
|
90
|
+
return typeof value === "string" ? value : null;
|
|
91
|
+
}
|
|
92
|
+
function readTextArray(row, key) {
|
|
93
|
+
const value = row[key];
|
|
94
|
+
if (!Array.isArray(value))
|
|
95
|
+
return [];
|
|
96
|
+
return value.filter((entry) => typeof entry === "string");
|
|
97
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FacilisDb } from "./client.js";
|
|
2
|
+
/** Bookkeeping table owned by this package, not by the project schema: introspection skips it. */
|
|
3
|
+
export declare const MIGRATION_TRACKING_TABLE = "_facilis_applied_statements";
|
|
4
|
+
export type MigrationSource = string | readonly string[] | {
|
|
5
|
+
readonly directory: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Applies statements in order inside one transaction. Statements are keyed by content
|
|
9
|
+
* hash and skipped when already applied, so bootstrapping a persistent dataDir twice is
|
|
10
|
+
* safe; a failure rolls back both the statements and the tracking rows.
|
|
11
|
+
*
|
|
12
|
+
* Array entries may either be one SQL statement or a whole file separated by drizzle-kit's
|
|
13
|
+
* "--> statement-breakpoint" marker (plain semicolons are never used as separators).
|
|
14
|
+
*/
|
|
15
|
+
export declare function applyStatements(db: FacilisDb, statements: readonly string[]): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Convenience runner for tests/examples: plain SQL text, a statement list, or a directory
|
|
18
|
+
* of drizzle-kit `.sql` files (applied in filename order).
|
|
19
|
+
*/
|
|
20
|
+
export declare function applyMigrations(db: FacilisDb, source: MigrationSource): Promise<void>;
|
package/dist/migrator.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sql } from "drizzle-orm";
|
|
4
|
+
/** drizzle-kit separates statements in generated SQL files with this marker. */
|
|
5
|
+
const STATEMENT_BREAKPOINT = "--> statement-breakpoint";
|
|
6
|
+
/** Bookkeeping table owned by this package, not by the project schema: introspection skips it. */
|
|
7
|
+
export const MIGRATION_TRACKING_TABLE = "_facilis_applied_statements";
|
|
8
|
+
/**
|
|
9
|
+
* Applies statements in order inside one transaction. Statements are keyed by content
|
|
10
|
+
* hash and skipped when already applied, so bootstrapping a persistent dataDir twice is
|
|
11
|
+
* safe; a failure rolls back both the statements and the tracking rows.
|
|
12
|
+
*
|
|
13
|
+
* Array entries may either be one SQL statement or a whole file separated by drizzle-kit's
|
|
14
|
+
* "--> statement-breakpoint" marker (plain semicolons are never used as separators).
|
|
15
|
+
*/
|
|
16
|
+
export async function applyStatements(db, statements) {
|
|
17
|
+
await db.db.transaction(async (tx) => {
|
|
18
|
+
await tx.execute(sql.raw(`CREATE TABLE IF NOT EXISTS ${MIGRATION_TRACKING_TABLE} (id text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`));
|
|
19
|
+
for (const statement of statements.flatMap(splitStatementBatch)) {
|
|
20
|
+
const trimmed = statement.trim();
|
|
21
|
+
if (trimmed.length === 0)
|
|
22
|
+
continue;
|
|
23
|
+
// no-await-in-loop: statements must run one at a time on the transaction's connection.
|
|
24
|
+
// eslint-disable-next-line no-await-in-loop
|
|
25
|
+
const hashed = await tx.execute(sql `select md5(${trimmed}) as id`);
|
|
26
|
+
const row = hashed.rows[0];
|
|
27
|
+
if (row === undefined || typeof row.id !== "string") {
|
|
28
|
+
throw new Error("applyStatements: failed to hash a migration statement");
|
|
29
|
+
}
|
|
30
|
+
const id = row.id;
|
|
31
|
+
// eslint-disable-next-line no-await-in-loop
|
|
32
|
+
const seen = await tx.execute(sql `select 1 from ${sql.identifier(MIGRATION_TRACKING_TABLE)} where id = ${id}`);
|
|
33
|
+
if (seen.rows.length > 0)
|
|
34
|
+
continue;
|
|
35
|
+
// eslint-disable-next-line no-await-in-loop
|
|
36
|
+
await tx.execute(sql.raw(trimmed));
|
|
37
|
+
// eslint-disable-next-line no-await-in-loop
|
|
38
|
+
await tx.execute(sql `insert into ${sql.identifier(MIGRATION_TRACKING_TABLE)} (id) values (${id})`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Convenience runner for tests/examples: plain SQL text, a statement list, or a directory
|
|
44
|
+
* of drizzle-kit `.sql` files (applied in filename order).
|
|
45
|
+
*/
|
|
46
|
+
export async function applyMigrations(db, source) {
|
|
47
|
+
const statements = typeof source === "string"
|
|
48
|
+
? [source]
|
|
49
|
+
: isMigrationDirectory(source)
|
|
50
|
+
? readMigrationDirectory(source.directory)
|
|
51
|
+
: source;
|
|
52
|
+
await applyStatements(db, statements);
|
|
53
|
+
}
|
|
54
|
+
function splitStatementBatch(batch) {
|
|
55
|
+
return batch.split(STATEMENT_BREAKPOINT);
|
|
56
|
+
}
|
|
57
|
+
function isMigrationDirectory(source) {
|
|
58
|
+
return typeof source === "object" && "directory" in source;
|
|
59
|
+
}
|
|
60
|
+
function readMigrationDirectory(directory) {
|
|
61
|
+
const files = readdirSync(directory, { withFileTypes: true })
|
|
62
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".sql"))
|
|
63
|
+
.map((entry) => entry.name)
|
|
64
|
+
.toSorted();
|
|
65
|
+
return files.map((file) => readFileSync(join(directory, file), "utf8"));
|
|
66
|
+
}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FacilisDb, PgDatabaseLike } from "./client.js";
|
|
2
|
+
/** Non-superuser role used for policy-scoped work; RLS is bypassed by superusers (D-008). */
|
|
3
|
+
export declare const APP_ROLE = "facilis_app";
|
|
4
|
+
/** Values exported to Postgres GUCs for one policy-scoped operation (contract §4.2). */
|
|
5
|
+
export interface PolicyScope {
|
|
6
|
+
readonly tenantId?: string;
|
|
7
|
+
readonly userId?: string;
|
|
8
|
+
readonly roles?: readonly string[];
|
|
9
|
+
/** The session's expanded permissions, read by permission views (D-098). */
|
|
10
|
+
readonly permissions?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/** Transaction handed to `runWithPolicyScope` callbacks. */
|
|
13
|
+
export type PolicyScopedTransaction = Parameters<Parameters<PgDatabaseLike["transaction"]>[0]>[0];
|
|
14
|
+
/** Drizzle database or transaction accepted by every scoped query helper. */
|
|
15
|
+
export type PolicyScopedDatabase = PgDatabaseLike | PolicyScopedTransaction;
|
|
16
|
+
/** Thrown when the app role has not been bootstrapped yet (`ensureAppRole`). */
|
|
17
|
+
export declare class AppRoleUnavailableError extends Error {
|
|
18
|
+
readonly code = "app_role_unavailable";
|
|
19
|
+
constructor(cause: unknown);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The classification `ensureAppRole` derives from the live schema: every table in `public`
|
|
23
|
+
* is either RLS-scoped (the app role has DML and the policies decide the rows) or
|
|
24
|
+
* system-only (no table privilege at all; reachable only through the system identity).
|
|
25
|
+
* D-136 makes this the invariant: a table without RLS is never app-reachable.
|
|
26
|
+
*/
|
|
27
|
+
export interface AppRoleProvisioning {
|
|
28
|
+
readonly systemOnlyTables: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates the non-superuser role that policy-scoped queries run as and reconciles its
|
|
32
|
+
* privileges with the live schema: **RLS is the grant boundary** (D-136). A table with RLS
|
|
33
|
+
* enabled gets DML for the app role (its policies then decide the rows); a table without RLS
|
|
34
|
+
* gets no table privilege at all, so only the system identity can touch it. Sequences stay
|
|
35
|
+
* granted to both classes: USAGE can neither read nor write a table, and serial/identity
|
|
36
|
+
* columns need it for inserts. Idempotent and re-asserting: safe on every boot, and the
|
|
37
|
+
* blanket grants an older cluster carries are revoked the next time this runs.
|
|
38
|
+
*
|
|
39
|
+
* The role is `NOINHERIT` on purpose. It is a member of `facilis_system` so a deployment
|
|
40
|
+
* whose connection already is `facilis_app` can `SET ROLE` for the audited elevation
|
|
41
|
+
* (D-099); membership must not hand the request identity the system role's grants, or the
|
|
42
|
+
* system-only tables would be reachable from every handler. `SET ROLE` still works with
|
|
43
|
+
* `NOINHERIT`: only implicit inheritance of the member role's privileges is disabled.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ensureAppRole(db: FacilisDb): Promise<AppRoleProvisioning>;
|
|
46
|
+
/**
|
|
47
|
+
* Runs `fn` in one transaction whose Postgres identity is facilis_app and whose GUCs carry
|
|
48
|
+
* the scope. GUC values travel through set_config parameters, never string concatenation.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runWithPolicyScope<T>(db: FacilisDb, scope: PolicyScope, fn: (tx: PolicyScopedDatabase) => Promise<T>): Promise<T>;
|