@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/tasks.d.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/** Every facilis_tasks.status value, in lifecycle order. */
|
|
2
|
+
export declare const FACILIS_TASK_STATUSES: readonly ["pending", "decided", "expired", "cancelled"];
|
|
3
|
+
export type FacilisTaskStatus = (typeof FACILIS_TASK_STATUSES)[number];
|
|
4
|
+
/** What the task asks for; approvals are the only kind the v1 slice ships. */
|
|
5
|
+
export declare const FACILIS_TASK_KINDS: readonly ["approval"];
|
|
6
|
+
export type FacilisTaskKind = (typeof FACILIS_TASK_KINDS)[number];
|
|
7
|
+
/** How a decided task closes. */
|
|
8
|
+
export declare const FACILIS_TASK_DECISIONS: readonly ["approve", "reject"];
|
|
9
|
+
export type FacilisTaskDecision = (typeof FACILIS_TASK_DECISIONS)[number];
|
|
10
|
+
/**
|
|
11
|
+
* One manual task. Statuses stay text (a new state is a code change, not a migration) and the
|
|
12
|
+
* workflow columns are nullable so a task can exist standalone before the workflow layer lands
|
|
13
|
+
* (D-135 W1).
|
|
14
|
+
*/
|
|
15
|
+
export declare const facilisTasks: Omit<import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
16
|
+
name: "facilis_tasks";
|
|
17
|
+
schema: undefined;
|
|
18
|
+
columns: {
|
|
19
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
20
|
+
name: "id";
|
|
21
|
+
tableName: "facilis_tasks";
|
|
22
|
+
dataType: "string";
|
|
23
|
+
columnType: "PgUUID";
|
|
24
|
+
data: string;
|
|
25
|
+
driverParam: string;
|
|
26
|
+
notNull: true;
|
|
27
|
+
hasDefault: true;
|
|
28
|
+
isPrimaryKey: true;
|
|
29
|
+
isAutoincrement: false;
|
|
30
|
+
hasRuntimeDefault: false;
|
|
31
|
+
enumValues: undefined;
|
|
32
|
+
baseColumn: never;
|
|
33
|
+
identity: undefined;
|
|
34
|
+
generated: undefined;
|
|
35
|
+
}, {}, {}>;
|
|
36
|
+
workflowId: import("drizzle-orm/pg-core").PgColumn<{
|
|
37
|
+
name: "workflow_id";
|
|
38
|
+
tableName: "facilis_tasks";
|
|
39
|
+
dataType: "string";
|
|
40
|
+
columnType: "PgUUID";
|
|
41
|
+
data: string;
|
|
42
|
+
driverParam: string;
|
|
43
|
+
notNull: false;
|
|
44
|
+
hasDefault: false;
|
|
45
|
+
isPrimaryKey: false;
|
|
46
|
+
isAutoincrement: false;
|
|
47
|
+
hasRuntimeDefault: false;
|
|
48
|
+
enumValues: undefined;
|
|
49
|
+
baseColumn: never;
|
|
50
|
+
identity: undefined;
|
|
51
|
+
generated: undefined;
|
|
52
|
+
}, {}, {}>;
|
|
53
|
+
stepIndex: import("drizzle-orm/pg-core").PgColumn<{
|
|
54
|
+
name: "step_index";
|
|
55
|
+
tableName: "facilis_tasks";
|
|
56
|
+
dataType: "number";
|
|
57
|
+
columnType: "PgInteger";
|
|
58
|
+
data: number;
|
|
59
|
+
driverParam: string | number;
|
|
60
|
+
notNull: false;
|
|
61
|
+
hasDefault: false;
|
|
62
|
+
isPrimaryKey: false;
|
|
63
|
+
isAutoincrement: false;
|
|
64
|
+
hasRuntimeDefault: false;
|
|
65
|
+
enumValues: undefined;
|
|
66
|
+
baseColumn: never;
|
|
67
|
+
identity: undefined;
|
|
68
|
+
generated: undefined;
|
|
69
|
+
}, {}, {}>;
|
|
70
|
+
kind: import("drizzle-orm/pg-core").PgColumn<{
|
|
71
|
+
name: "kind";
|
|
72
|
+
tableName: "facilis_tasks";
|
|
73
|
+
dataType: "string";
|
|
74
|
+
columnType: "PgText";
|
|
75
|
+
data: string;
|
|
76
|
+
driverParam: string;
|
|
77
|
+
notNull: true;
|
|
78
|
+
hasDefault: false;
|
|
79
|
+
isPrimaryKey: false;
|
|
80
|
+
isAutoincrement: false;
|
|
81
|
+
hasRuntimeDefault: false;
|
|
82
|
+
enumValues: [string, ...string[]];
|
|
83
|
+
baseColumn: never;
|
|
84
|
+
identity: undefined;
|
|
85
|
+
generated: undefined;
|
|
86
|
+
}, {}, {}>;
|
|
87
|
+
status: import("drizzle-orm/pg-core").PgColumn<{
|
|
88
|
+
name: "status";
|
|
89
|
+
tableName: "facilis_tasks";
|
|
90
|
+
dataType: "string";
|
|
91
|
+
columnType: "PgText";
|
|
92
|
+
data: string;
|
|
93
|
+
driverParam: string;
|
|
94
|
+
notNull: true;
|
|
95
|
+
hasDefault: false;
|
|
96
|
+
isPrimaryKey: false;
|
|
97
|
+
isAutoincrement: false;
|
|
98
|
+
hasRuntimeDefault: false;
|
|
99
|
+
enumValues: [string, ...string[]];
|
|
100
|
+
baseColumn: never;
|
|
101
|
+
identity: undefined;
|
|
102
|
+
generated: undefined;
|
|
103
|
+
}, {}, {}>;
|
|
104
|
+
decision: import("drizzle-orm/pg-core").PgColumn<{
|
|
105
|
+
name: "decision";
|
|
106
|
+
tableName: "facilis_tasks";
|
|
107
|
+
dataType: "string";
|
|
108
|
+
columnType: "PgText";
|
|
109
|
+
data: string;
|
|
110
|
+
driverParam: string;
|
|
111
|
+
notNull: false;
|
|
112
|
+
hasDefault: false;
|
|
113
|
+
isPrimaryKey: false;
|
|
114
|
+
isAutoincrement: false;
|
|
115
|
+
hasRuntimeDefault: false;
|
|
116
|
+
enumValues: [string, ...string[]];
|
|
117
|
+
baseColumn: never;
|
|
118
|
+
identity: undefined;
|
|
119
|
+
generated: undefined;
|
|
120
|
+
}, {}, {}>;
|
|
121
|
+
titleKey: import("drizzle-orm/pg-core").PgColumn<{
|
|
122
|
+
name: "title_key";
|
|
123
|
+
tableName: "facilis_tasks";
|
|
124
|
+
dataType: "string";
|
|
125
|
+
columnType: "PgText";
|
|
126
|
+
data: string;
|
|
127
|
+
driverParam: string;
|
|
128
|
+
notNull: true;
|
|
129
|
+
hasDefault: false;
|
|
130
|
+
isPrimaryKey: false;
|
|
131
|
+
isAutoincrement: false;
|
|
132
|
+
hasRuntimeDefault: false;
|
|
133
|
+
enumValues: [string, ...string[]];
|
|
134
|
+
baseColumn: never;
|
|
135
|
+
identity: undefined;
|
|
136
|
+
generated: undefined;
|
|
137
|
+
}, {}, {}>;
|
|
138
|
+
titleParams: import("drizzle-orm/pg-core").PgColumn<{
|
|
139
|
+
name: "title_params";
|
|
140
|
+
tableName: "facilis_tasks";
|
|
141
|
+
dataType: "json";
|
|
142
|
+
columnType: "PgJsonb";
|
|
143
|
+
data: unknown;
|
|
144
|
+
driverParam: unknown;
|
|
145
|
+
notNull: true;
|
|
146
|
+
hasDefault: true;
|
|
147
|
+
isPrimaryKey: false;
|
|
148
|
+
isAutoincrement: false;
|
|
149
|
+
hasRuntimeDefault: false;
|
|
150
|
+
enumValues: undefined;
|
|
151
|
+
baseColumn: never;
|
|
152
|
+
identity: undefined;
|
|
153
|
+
generated: undefined;
|
|
154
|
+
}, {}, {}>;
|
|
155
|
+
payload: import("drizzle-orm/pg-core").PgColumn<{
|
|
156
|
+
name: "payload";
|
|
157
|
+
tableName: "facilis_tasks";
|
|
158
|
+
dataType: "json";
|
|
159
|
+
columnType: "PgJsonb";
|
|
160
|
+
data: unknown;
|
|
161
|
+
driverParam: unknown;
|
|
162
|
+
notNull: false;
|
|
163
|
+
hasDefault: false;
|
|
164
|
+
isPrimaryKey: false;
|
|
165
|
+
isAutoincrement: false;
|
|
166
|
+
hasRuntimeDefault: false;
|
|
167
|
+
enumValues: undefined;
|
|
168
|
+
baseColumn: never;
|
|
169
|
+
identity: undefined;
|
|
170
|
+
generated: undefined;
|
|
171
|
+
}, {}, {}>;
|
|
172
|
+
requestedBy: import("drizzle-orm/pg-core").PgColumn<{
|
|
173
|
+
name: "requested_by";
|
|
174
|
+
tableName: "facilis_tasks";
|
|
175
|
+
dataType: "string";
|
|
176
|
+
columnType: "PgText";
|
|
177
|
+
data: string;
|
|
178
|
+
driverParam: string;
|
|
179
|
+
notNull: false;
|
|
180
|
+
hasDefault: false;
|
|
181
|
+
isPrimaryKey: false;
|
|
182
|
+
isAutoincrement: false;
|
|
183
|
+
hasRuntimeDefault: false;
|
|
184
|
+
enumValues: [string, ...string[]];
|
|
185
|
+
baseColumn: never;
|
|
186
|
+
identity: undefined;
|
|
187
|
+
generated: undefined;
|
|
188
|
+
}, {}, {}>;
|
|
189
|
+
assignedPermission: import("drizzle-orm/pg-core").PgColumn<{
|
|
190
|
+
name: "assigned_permission";
|
|
191
|
+
tableName: "facilis_tasks";
|
|
192
|
+
dataType: "string";
|
|
193
|
+
columnType: "PgText";
|
|
194
|
+
data: string;
|
|
195
|
+
driverParam: string;
|
|
196
|
+
notNull: false;
|
|
197
|
+
hasDefault: false;
|
|
198
|
+
isPrimaryKey: false;
|
|
199
|
+
isAutoincrement: false;
|
|
200
|
+
hasRuntimeDefault: false;
|
|
201
|
+
enumValues: [string, ...string[]];
|
|
202
|
+
baseColumn: never;
|
|
203
|
+
identity: undefined;
|
|
204
|
+
generated: undefined;
|
|
205
|
+
}, {}, {}>;
|
|
206
|
+
expiresAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
207
|
+
name: "expires_at";
|
|
208
|
+
tableName: "facilis_tasks";
|
|
209
|
+
dataType: "date";
|
|
210
|
+
columnType: "PgTimestamp";
|
|
211
|
+
data: Date;
|
|
212
|
+
driverParam: string;
|
|
213
|
+
notNull: false;
|
|
214
|
+
hasDefault: false;
|
|
215
|
+
isPrimaryKey: false;
|
|
216
|
+
isAutoincrement: false;
|
|
217
|
+
hasRuntimeDefault: false;
|
|
218
|
+
enumValues: undefined;
|
|
219
|
+
baseColumn: never;
|
|
220
|
+
identity: undefined;
|
|
221
|
+
generated: undefined;
|
|
222
|
+
}, {}, {}>;
|
|
223
|
+
decidedBy: import("drizzle-orm/pg-core").PgColumn<{
|
|
224
|
+
name: "decided_by";
|
|
225
|
+
tableName: "facilis_tasks";
|
|
226
|
+
dataType: "string";
|
|
227
|
+
columnType: "PgText";
|
|
228
|
+
data: string;
|
|
229
|
+
driverParam: string;
|
|
230
|
+
notNull: false;
|
|
231
|
+
hasDefault: false;
|
|
232
|
+
isPrimaryKey: false;
|
|
233
|
+
isAutoincrement: false;
|
|
234
|
+
hasRuntimeDefault: false;
|
|
235
|
+
enumValues: [string, ...string[]];
|
|
236
|
+
baseColumn: never;
|
|
237
|
+
identity: undefined;
|
|
238
|
+
generated: undefined;
|
|
239
|
+
}, {}, {}>;
|
|
240
|
+
decidedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
241
|
+
name: "decided_at";
|
|
242
|
+
tableName: "facilis_tasks";
|
|
243
|
+
dataType: "date";
|
|
244
|
+
columnType: "PgTimestamp";
|
|
245
|
+
data: Date;
|
|
246
|
+
driverParam: string;
|
|
247
|
+
notNull: false;
|
|
248
|
+
hasDefault: false;
|
|
249
|
+
isPrimaryKey: false;
|
|
250
|
+
isAutoincrement: false;
|
|
251
|
+
hasRuntimeDefault: false;
|
|
252
|
+
enumValues: undefined;
|
|
253
|
+
baseColumn: never;
|
|
254
|
+
identity: undefined;
|
|
255
|
+
generated: undefined;
|
|
256
|
+
}, {}, {}>;
|
|
257
|
+
comment: import("drizzle-orm/pg-core").PgColumn<{
|
|
258
|
+
name: "comment";
|
|
259
|
+
tableName: "facilis_tasks";
|
|
260
|
+
dataType: "string";
|
|
261
|
+
columnType: "PgText";
|
|
262
|
+
data: string;
|
|
263
|
+
driverParam: string;
|
|
264
|
+
notNull: false;
|
|
265
|
+
hasDefault: false;
|
|
266
|
+
isPrimaryKey: false;
|
|
267
|
+
isAutoincrement: false;
|
|
268
|
+
hasRuntimeDefault: false;
|
|
269
|
+
enumValues: [string, ...string[]];
|
|
270
|
+
baseColumn: never;
|
|
271
|
+
identity: undefined;
|
|
272
|
+
generated: undefined;
|
|
273
|
+
}, {}, {}>;
|
|
274
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
275
|
+
name: "created_at";
|
|
276
|
+
tableName: "facilis_tasks";
|
|
277
|
+
dataType: "date";
|
|
278
|
+
columnType: "PgTimestamp";
|
|
279
|
+
data: Date;
|
|
280
|
+
driverParam: string;
|
|
281
|
+
notNull: true;
|
|
282
|
+
hasDefault: true;
|
|
283
|
+
isPrimaryKey: false;
|
|
284
|
+
isAutoincrement: false;
|
|
285
|
+
hasRuntimeDefault: false;
|
|
286
|
+
enumValues: undefined;
|
|
287
|
+
baseColumn: never;
|
|
288
|
+
identity: undefined;
|
|
289
|
+
generated: undefined;
|
|
290
|
+
}, {}, {}>;
|
|
291
|
+
tenantId: import("drizzle-orm/pg-core").PgColumn<{
|
|
292
|
+
name: string;
|
|
293
|
+
tableName: "facilis_tasks";
|
|
294
|
+
dataType: "string";
|
|
295
|
+
columnType: "PgUUID";
|
|
296
|
+
data: string;
|
|
297
|
+
driverParam: string;
|
|
298
|
+
notNull: true;
|
|
299
|
+
hasDefault: true;
|
|
300
|
+
isPrimaryKey: false;
|
|
301
|
+
isAutoincrement: false;
|
|
302
|
+
hasRuntimeDefault: false;
|
|
303
|
+
enumValues: undefined;
|
|
304
|
+
baseColumn: never;
|
|
305
|
+
identity: undefined;
|
|
306
|
+
generated: undefined;
|
|
307
|
+
}, {}, {}>;
|
|
308
|
+
};
|
|
309
|
+
dialect: 'pg';
|
|
310
|
+
}>, "enableRLS">;
|
|
311
|
+
export type FacilisTaskRow = typeof facilisTasks.$inferSelect;
|
|
312
|
+
export type NewFacilisTaskRow = typeof facilisTasks.$inferInsert;
|
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The manual-task table (D-135, docs/WORKFLOWS.md §5).
|
|
2
|
+
//
|
|
3
|
+
// A task is framework state, not application data: tenant-scoped so the structural invariant
|
|
4
|
+
// holds (pgPolicy + ENABLE ROW LEVEL SECURITY), re-exported by the project's db/schema.ts, and
|
|
5
|
+
// DDL-owned by a Drizzle migration like the job queue (D-107).
|
|
6
|
+
import { sql } from "drizzle-orm";
|
|
7
|
+
import { index, integer, jsonb, pgPolicy, pgTable, text, timestamp, uuid, } from "drizzle-orm/pg-core";
|
|
8
|
+
import { tenantIsolationPolicy } from "./rls.js";
|
|
9
|
+
import { tenantColumn } from "./tenant-column.js";
|
|
10
|
+
/** Every facilis_tasks.status value, in lifecycle order. */
|
|
11
|
+
export const FACILIS_TASK_STATUSES = ["pending", "decided", "expired", "cancelled"];
|
|
12
|
+
/** What the task asks for; approvals are the only kind the v1 slice ships. */
|
|
13
|
+
export const FACILIS_TASK_KINDS = ["approval"];
|
|
14
|
+
/** How a decided task closes. */
|
|
15
|
+
export const FACILIS_TASK_DECISIONS = ["approve", "reject"];
|
|
16
|
+
/**
|
|
17
|
+
* One manual task. Statuses stay text (a new state is a code change, not a migration) and the
|
|
18
|
+
* workflow columns are nullable so a task can exist standalone before the workflow layer lands
|
|
19
|
+
* (D-135 W1).
|
|
20
|
+
*/
|
|
21
|
+
export const facilisTasks = pgTable("facilis_tasks", {
|
|
22
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
23
|
+
workflowId: uuid("workflow_id"),
|
|
24
|
+
stepIndex: integer("step_index"),
|
|
25
|
+
kind: text("kind").notNull(),
|
|
26
|
+
status: text("status").notNull(),
|
|
27
|
+
decision: text("decision"),
|
|
28
|
+
titleKey: text("title_key").notNull(),
|
|
29
|
+
titleParams: jsonb("title_params").notNull().default({}),
|
|
30
|
+
payload: jsonb("payload"),
|
|
31
|
+
requestedBy: text("requested_by"),
|
|
32
|
+
assignedPermission: text("assigned_permission"),
|
|
33
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
34
|
+
decidedBy: text("decided_by"),
|
|
35
|
+
decidedAt: timestamp("decided_at", { withTimezone: true }),
|
|
36
|
+
comment: text("comment"),
|
|
37
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
38
|
+
tenantId: tenantColumn(),
|
|
39
|
+
}, (table) => [
|
|
40
|
+
tenantIsolationPolicy(),
|
|
41
|
+
// A RESTRICTIVE policy only narrows what a permissive policy already allows; with no
|
|
42
|
+
// permissive policy Postgres denies every row (D-008). The queue's reads/writes run as
|
|
43
|
+
// facilis_app, so the scoped tenant needs this gate — exactly like facilis_jobs.
|
|
44
|
+
pgPolicy("facilis_tasks_tenant_access", {
|
|
45
|
+
for: "all",
|
|
46
|
+
using: sql `nullif(current_setting('app.tenant_id', true), '') IS NOT NULL`,
|
|
47
|
+
withCheck: sql `nullif(current_setting('app.tenant_id', true), '') IS NOT NULL`,
|
|
48
|
+
}),
|
|
49
|
+
index("facilis_tasks_status_created_idx").on(table.status, table.createdAt),
|
|
50
|
+
index("facilis_tasks_workflow_idx").on(table.workflowId),
|
|
51
|
+
index("facilis_tasks_assigned_idx").on(table.assignedPermission, table.status),
|
|
52
|
+
]).enableRLS();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL expression that fills a tenant column from the policy-scope GUC.
|
|
3
|
+
*
|
|
4
|
+
* It reads the same `app.tenant_id` every RLS policy reads, transaction-locally set by
|
|
5
|
+
* `runWithPolicyScope` before the handler runs (contracts §4.2). The `nullif` guard is the
|
|
6
|
+
* same one the policies use: after a `RESET` a GUC reads as `''`, and `''::uuid` raises.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tenantDefaultSql(): import("drizzle-orm").SQL<unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* A `tenant_id` column that takes its value from the policy scope (D-040).
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* export const todos = pgTable("todos", { tenantId: tenantColumn(), ... }, ...).enableRLS();
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The default is the DB's, not the app's: an insert never names the tenant (raw SQL included),
|
|
17
|
+
* and it cannot name a different one — the RESTRICTIVE tenant policy still checks the written
|
|
18
|
+
* row against the same GUC. It also makes the column optional in Drizzle's insert type, so app
|
|
19
|
+
* code stops passing `tenantId: auth.tenantId` and the compiler stops asking for it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function tenantColumn(name?: string): import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgUUIDBuilderInitial<string>>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { uuid } from "drizzle-orm/pg-core";
|
|
3
|
+
/**
|
|
4
|
+
* SQL expression that fills a tenant column from the policy-scope GUC.
|
|
5
|
+
*
|
|
6
|
+
* It reads the same `app.tenant_id` every RLS policy reads, transaction-locally set by
|
|
7
|
+
* `runWithPolicyScope` before the handler runs (contracts §4.2). The `nullif` guard is the
|
|
8
|
+
* same one the policies use: after a `RESET` a GUC reads as `''`, and `''::uuid` raises.
|
|
9
|
+
*/
|
|
10
|
+
export function tenantDefaultSql() {
|
|
11
|
+
return sql `nullif(current_setting('app.tenant_id', true), '')::uuid`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A `tenant_id` column that takes its value from the policy scope (D-040).
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* export const todos = pgTable("todos", { tenantId: tenantColumn(), ... }, ...).enableRLS();
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The default is the DB's, not the app's: an insert never names the tenant (raw SQL included),
|
|
21
|
+
* and it cannot name a different one — the RESTRICTIVE tenant policy still checks the written
|
|
22
|
+
* row against the same GUC. It also makes the column optional in Drizzle's insert type, so app
|
|
23
|
+
* code stops passing `tenantId: auth.tenantId` and the compiler stops asking for it.
|
|
24
|
+
*/
|
|
25
|
+
export function tenantColumn(name = "tenant_id") {
|
|
26
|
+
return uuid(name).notNull().default(tenantDefaultSql());
|
|
27
|
+
}
|
package/dist/views.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type SQL } from "drizzle-orm";
|
|
2
|
+
import { type ManualViewBuilder, type PgColumnBuilderBase } from "drizzle-orm/pg-core";
|
|
3
|
+
import type { FacilisDb } from "./client.js";
|
|
4
|
+
/** Brand carrier for a view object, so a declaration is recognisable from its export. */
|
|
5
|
+
export declare const VIEW_BRAND: unique symbol;
|
|
6
|
+
/** SQL view names are unquoted identifiers; generated code always satisfies this. */
|
|
7
|
+
export declare const VIEW_NAME_PATTERN: RegExp;
|
|
8
|
+
/** A view's own permission, when declared. Same identifier shape the config uses. */
|
|
9
|
+
export declare const VIEW_PERMISSION_PATTERN: RegExp;
|
|
10
|
+
export interface ViewMetadata {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly query: SQL;
|
|
13
|
+
/** Underlying tables, for manifest/graph metadata. Informational only. */
|
|
14
|
+
readonly tables: readonly string[];
|
|
15
|
+
/** The view's own permission; null keeps the D-097 invoker behaviour (D-098). */
|
|
16
|
+
readonly permission: string | null;
|
|
17
|
+
/** "definer" exactly when a permission is set, "invoker" otherwise. */
|
|
18
|
+
readonly access: "invoker" | "definer";
|
|
19
|
+
}
|
|
20
|
+
export interface DefineViewOptions<TName extends string = string, TColumns extends Record<string, PgColumnBuilderBase> = Record<string, PgColumnBuilderBase>> {
|
|
21
|
+
/** SQL view name: ^[a-z][a-z0-9_]{0,62}$ */
|
|
22
|
+
readonly name: TName;
|
|
23
|
+
/** Column builders; keys must equal the SQL column names. */
|
|
24
|
+
readonly columns: TColumns;
|
|
25
|
+
/** Literal, read-only SELECT/WITH/VALUES/TABLE. Values must not be interpolated. */
|
|
26
|
+
readonly query: SQL;
|
|
27
|
+
/** Underlying tables the query selects (metadata only). */
|
|
28
|
+
readonly tables?: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* Optional view permission (D-098). With it the view becomes a definer view
|
|
31
|
+
* (security_invoker = false) and the declared permission alone decides access; the base
|
|
32
|
+
* tables' permissions are not consulted. Omit it for the D-097 invoker behaviour.
|
|
33
|
+
*/
|
|
34
|
+
readonly permission?: string;
|
|
35
|
+
}
|
|
36
|
+
/** A Drizzle view usable in `db.select().from(...)`, branded with its declaration. */
|
|
37
|
+
export type FacilisView<TName extends string, TColumns extends Record<string, PgColumnBuilderBase>> = ReturnType<ManualViewBuilder<TName, TColumns>["existing"]> & {
|
|
38
|
+
readonly [VIEW_BRAND]: ViewMetadata;
|
|
39
|
+
};
|
|
40
|
+
export type ViewErrorCode = "invalid_name" | "invalid_sql" | "invalid_permission";
|
|
41
|
+
/** A declaration that cannot become a database view; the code reaches the dev endpoint. */
|
|
42
|
+
export declare class ViewDefinitionError extends Error {
|
|
43
|
+
readonly code: ViewErrorCode;
|
|
44
|
+
constructor(code: ViewErrorCode, message: string);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Declare a SQL view. Never throws: an invalid declaration is reported when the framework
|
|
48
|
+
* tries to create it, so a typo in db/views cannot take the dev server down at import time.
|
|
49
|
+
*/
|
|
50
|
+
export declare function defineView<const TName extends string, const TColumns extends Record<string, PgColumnBuilderBase>>(options: DefineViewOptions<TName, TColumns>): FacilisView<TName, TColumns>;
|
|
51
|
+
/** The declaration carried by a view object, when `value` is one. */
|
|
52
|
+
export declare function viewMetadata(value: unknown): ViewMetadata | null;
|
|
53
|
+
export declare function isFacilisView(value: unknown): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Declarations in one or more module namespaces, deduplicated by name (first wins).
|
|
56
|
+
* A module namespace can export anything, so non-view exports are skipped.
|
|
57
|
+
*/
|
|
58
|
+
export declare function collectViewMetadata(modules: ReadonlyArray<Record<string, unknown>> | Record<string, unknown>): ViewMetadata[];
|
|
59
|
+
/**
|
|
60
|
+
* CREATE OR REPLACE VIEW for a declaration. Rejects a non-identifier name and a query
|
|
61
|
+
* that is not a single static read-only statement: the SQL is inlined into the DDL, so
|
|
62
|
+
* this check is the boundary, not a style rule.
|
|
63
|
+
*/
|
|
64
|
+
export declare function viewCreateStatement(metadata: ViewMetadata): SQL;
|
|
65
|
+
/**
|
|
66
|
+
* CREATE OR REPLACE VIEW for a raw SQL text (the query console's create-view flow).
|
|
67
|
+
*
|
|
68
|
+
* With a permission the view is a definer view whose only gate is that permission: the
|
|
69
|
+
* outer wrapper applies it to any query shape, and the permission name is validated before
|
|
70
|
+
* it is inlined (the DDL has no bind parameters, by construction).
|
|
71
|
+
*/
|
|
72
|
+
export declare function viewDefinitionStatement(name: string, query: SQL | string, permission?: string): SQL;
|
|
73
|
+
/** Grants SELECT on a view to the policy-scoped app role. Idempotent. */
|
|
74
|
+
export declare function grantViewSelect(db: FacilisDb, name: string): Promise<void>;
|
|
75
|
+
export interface ViewApplyFailure {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly error: string;
|
|
78
|
+
}
|
|
79
|
+
export interface ViewApplyResult {
|
|
80
|
+
readonly created: readonly string[];
|
|
81
|
+
readonly failed: readonly ViewApplyFailure[];
|
|
82
|
+
}
|
|
83
|
+
export interface ViewApplyOptions {
|
|
84
|
+
/** Permissions declared in facilis.config.ts; a view naming another one fails as data. */
|
|
85
|
+
readonly permissions?: readonly string[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Creates every declared view, then retries the failures once: a view may select from
|
|
89
|
+
* another view declared later in the same module. A failure is data, never a throw, so a
|
|
90
|
+
* broken view degrades to a manifest warning like every other boot step.
|
|
91
|
+
*/
|
|
92
|
+
export declare function applyViews(db: FacilisDb, modules: ReadonlyArray<Record<string, unknown>> | Record<string, unknown>, options?: ViewApplyOptions): Promise<ViewApplyResult>;
|