@arivie/db-postgres 0.1.1 → 2.0.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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.1.2
2
+
3
+ ### Patch Changes
4
+
5
+ - Updated dependencies
6
+ - @arivie/core@1.1.0
7
+
1
8
  ## 0.1.1
2
9
 
3
10
  ### Patch Changes
package/dist/index.js CHANGED
@@ -1,236 +1,10 @@
1
- import { createHash } from 'crypto';
2
- import postgres from 'postgres';
3
- import { ArivieBoundaryError } from '@arivie/core/types';
4
- export { ArivieBoundaryError } from '@arivie/core/types';
5
-
6
- // src/adapter.ts
7
- var ToolError = class extends Error {
8
- constructor(kind, message) {
9
- super(message ?? kind);
10
- this.kind = kind;
11
- this.name = "ToolError";
12
- }
13
- kind;
14
- code = "ARIVIE_TOOL_ERROR";
15
- };
16
-
17
- // src/compile-metric.ts
18
- var FILTER_COL_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
19
- var ENTITY_COL_REF = /\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
20
- function isFilterPrimitive(v) {
21
- return typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null;
22
- }
23
- function collectEntityRefs(text) {
24
- const refs = /* @__PURE__ */ new Set();
25
- for (const match of text.matchAll(ENTITY_COL_REF)) {
26
- refs.add(match[1]);
27
- }
28
- return refs;
29
- }
30
- function detectJoinsNeeded(entity, dimensionSqls, filterKeys) {
31
- const joinTargets = new Set((entity.joins ?? []).map((j) => j.to));
32
- const refs = /* @__PURE__ */ new Set();
33
- for (const sql of dimensionSqls) {
34
- for (const ref of collectEntityRefs(sql)) {
35
- if (joinTargets.has(ref) && ref !== entity.name) {
36
- refs.add(ref);
37
- }
38
- }
39
- }
40
- for (const key of filterKeys) {
41
- const dot = key.indexOf(".");
42
- if (dot > 0) {
43
- const refEntity = key.slice(0, dot);
44
- if (joinTargets.has(refEntity) && refEntity !== entity.name) {
45
- refs.add(refEntity);
46
- }
47
- }
48
- }
49
- return refs;
50
- }
51
- function buildJoinClauses(entity, joinsNeeded) {
52
- const clauses = [];
53
- for (const otherEntity of joinsNeeded) {
54
- const matching = (entity.joins ?? []).filter((j) => j.to === otherEntity);
55
- if (matching.length === 0) {
56
- continue;
57
- }
58
- if (matching.length > 1) {
59
- throw new ToolError(
60
- "join-ambiguous",
61
- `multiple join paths to '${otherEntity}'; specify entityHint to disambiguate`
62
- );
63
- }
64
- const join = matching[0];
65
- clauses.push(`LEFT JOIN ${join.to} ON ${join.on}`);
66
- }
67
- return clauses;
68
- }
69
- function compileMetricForPostgres(opts) {
70
- const { entity, metric } = opts;
71
- const measure = entity.measures?.find((m) => m.name === metric);
72
- if (measure == null) {
73
- throw new ToolError(
74
- "metric-not-found",
75
- `metric '${metric}' not found on entity '${entity.name}'`
76
- );
77
- }
78
- const dimensionNames = opts.dimensions ?? [];
79
- const selectExprs = [`(${measure.sql}) AS "${measure.name}"`];
80
- const dimensionSqls = [];
81
- for (const dimName of dimensionNames) {
82
- const dim = entity.dimensions?.find((d) => d.name === dimName);
83
- if (dim == null) {
84
- throw new ToolError(
85
- "dimension-not-found",
86
- `dimension '${dimName}' not found on entity '${entity.name}'`
87
- );
88
- }
89
- selectExprs.push(`(${dim.sql}) AS "${dim.name}"`);
90
- dimensionSqls.push(dim.sql);
91
- }
92
- const filterKeys = Object.keys(opts.filters ?? {});
93
- const joinsNeeded = detectJoinsNeeded(entity, dimensionSqls, filterKeys);
94
- const joinClauses = buildJoinClauses(entity, joinsNeeded);
95
- const whereClauses = [];
96
- const params = [];
97
- for (const segName of opts.segments ?? []) {
98
- const seg = entity.segments?.find((s) => s.name === segName);
99
- if (seg == null) {
100
- throw new ToolError(
101
- "segment-not-found",
102
- `segment '${segName}' not found on entity '${entity.name}'`
103
- );
104
- }
105
- whereClauses.push(`(${seg.sql})`);
106
- }
107
- for (const [col, value] of Object.entries(opts.filters ?? {})) {
108
- if (!FILTER_COL_PATTERN.test(col)) {
109
- throw new ToolError(
110
- "filter-invalid",
111
- `filter column '${col}' must be a plain identifier`
112
- );
113
- }
114
- if (!isFilterPrimitive(value)) {
115
- throw new ToolError(
116
- "filter-invalid",
117
- `filter value for '${col}' must be string|number|boolean|null`
118
- );
119
- }
120
- if (value === null) {
121
- whereClauses.push(`${col} IS NULL`);
122
- } else {
123
- whereClauses.push(`${col} = $${params.length + 1}`);
124
- params.push(value);
125
- }
126
- }
127
- const fromTable = typeof entity.source === "string" ? entity.source : entity.name;
128
- const parts = [`SELECT ${selectExprs.join(", ")}`, `FROM ${fromTable}`];
129
- if (joinClauses.length > 0) {
130
- parts.push(...joinClauses);
131
- }
132
- if (whereClauses.length > 0) {
133
- parts.push(`WHERE ${whereClauses.join(" AND ")}`);
134
- }
135
- if (dimensionNames.length > 0) {
136
- parts.push(
137
- `GROUP BY ${dimensionNames.map((d) => `"${d}"`).join(", ")}`
138
- );
139
- }
140
- const query = parts.join(" ");
141
- return params.length > 0 ? { query, params } : { query };
142
- }
143
-
144
- // src/identifier.ts
145
- var IDENT_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
146
- function escapeIdent(name) {
147
- if (!IDENT_RE.test(name)) {
148
- throw new ToolError("sql-invalid", `invalid identifier: ${name}`);
149
- }
150
- return `"${name}"`;
151
- }
152
-
153
- // src/execute.ts
154
- function isPostgresError(err) {
155
- return typeof err === "object" && err !== null && "code" in err;
156
- }
157
- async function executeImpl(sql, opts) {
158
- const startedAt = Date.now();
159
- if (!Number.isFinite(opts.timeoutMs) || !Number.isInteger(opts.timeoutMs) || opts.timeoutMs <= 0) {
160
- throw new ToolError(
161
- "sql-invalid",
162
- `timeoutMs must be a positive integer; got ${String(opts.timeoutMs)}`
163
- );
164
- }
165
- if (!Number.isFinite(opts.rowLimit) || !Number.isInteger(opts.rowLimit) || opts.rowLimit <= 0) {
166
- throw new ToolError(
167
- "sql-invalid",
168
- `rowLimit must be a positive integer; got ${String(opts.rowLimit)}`
169
- );
170
- }
171
- if (opts.runAsRole == null || opts.runAsRole === "") {
172
- throw new ToolError("sql-invalid", "runAsRole is required");
173
- }
174
- const runAsRole = opts.runAsRole;
175
- try {
176
- const rows = await sql.begin(async (tx) => {
177
- await tx.unsafe(`SET LOCAL ROLE ${escapeIdent(runAsRole)}`);
178
- await tx.unsafe(
179
- `SET LOCAL statement_timeout = ${opts.timeoutMs}`
180
- );
181
- let queryParams;
182
- if (opts.params != null) {
183
- const copy = [...opts.params];
184
- for (let i = 0; i < copy.length; i++) {
185
- const v = copy[i];
186
- if (v !== null && typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") {
187
- throw new ToolError(
188
- "sql-invalid",
189
- `params[${i}] must be string|number|boolean|null; got ${typeof v}`
190
- );
191
- }
192
- }
193
- queryParams = copy;
194
- }
195
- return await tx.unsafe(opts.query, queryParams);
196
- });
197
- const truncated = rows.length > opts.rowLimit;
198
- const limited = rows.slice(0, opts.rowLimit);
199
- return {
200
- rows: limited,
201
- rowCount: limited.length,
202
- durationMs: Date.now() - startedAt,
203
- truncated
204
- };
205
- } catch (err) {
206
- if (isPostgresError(err)) {
207
- if (err.code === "42501") {
208
- throw new ToolError(
209
- "sql-permission-denied",
210
- "permission denied for SQL operation"
211
- );
212
- }
213
- if (err.code === "57014") {
214
- throw new ToolError("sql-timeout", "statement timeout");
215
- }
216
- }
217
- throw err;
218
- }
219
- }
220
-
221
- // src/introspect.ts
222
- var PII_RE = /email|phone|ssn|address|dob|password|secret|token|card/i;
223
- async function introspect(sql) {
224
- const tables = await sql`
1
+ import {createHash}from'crypto';import F from'postgres';import {ArivieBoundaryError}from'@arivie/core/types';export{ArivieBoundaryError}from'@arivie/core/types';var s=class extends Error{constructor(n,o){super(o??n);this.kind=n;this.name="ToolError";}kind;code="ARIVIE_TOOL_ERROR"};var L=/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/,$=/\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;function C(t){return typeof t=="string"||typeof t=="number"||typeof t=="boolean"||t===null}function I(t){let e=new Set;for(let n of t.matchAll($))e.add(n[1]);return e}function k(t,e,n){let o=new Set((t.joins??[]).map(r=>r.to)),i=new Set;for(let r of e)for(let c of I(r))o.has(c)&&c!==t.name&&i.add(c);for(let r of n){let c=r.indexOf(".");if(c>0){let l=r.slice(0,c);o.has(l)&&l!==t.name&&i.add(l);}}return i}function v(t,e){let n=[];for(let o of e){let i=(t.joins??[]).filter(c=>c.to===o);if(i.length===0)continue;if(i.length>1)throw new s("join-ambiguous",`multiple join paths to '${o}'; specify entityHint to disambiguate`);let r=i[0];n.push(`LEFT JOIN ${r.to} ON ${r.on}`);}return n}function b(t){let e=t.entity,{metric:n}=t,o=e.measures?.find(u=>u.name===n);if(o==null)throw new s("metric-not-found",`metric '${n}' not found on entity '${e.name}'`);let i=t.dimensions??[],r=[`(${o.sql}) AS "${o.name}"`],c=[];for(let u of i){let f=e.dimensions?.find(w=>w.name===u);if(f==null)throw new s("dimension-not-found",`dimension '${u}' not found on entity '${e.name}'`);r.push(`(${f.sql}) AS "${f.name}"`),c.push(f.sql);}let l=Object.keys(t.filters??{}),p=k(e,c,l),d=v(e,p),a=[],m=[];for(let u of t.segments??[]){let f=e.segments?.find(w=>w.name===u);if(f==null)throw new s("segment-not-found",`segment '${u}' not found on entity '${e.name}'`);a.push(`(${f.sql})`);}for(let[u,f]of Object.entries(t.filters??{})){if(!L.test(u))throw new s("filter-invalid",`filter column '${u}' must be a plain identifier`);if(!C(f))throw new s("filter-invalid",`filter value for '${u}' must be string|number|boolean|null`);f===null?a.push(`${u} IS NULL`):(a.push(`${u} = $${m.length+1}`),m.push(f));}let S=typeof e.source=="string"?e.source:e.name,g=[`SELECT ${r.join(", ")}`,`FROM ${S}`];d.length>0&&g.push(...d),a.length>0&&g.push(`WHERE ${a.join(" AND ")}`),i.length>0&&g.push(`GROUP BY ${i.map(u=>`"${u}"`).join(", ")}`);let h=g.join(" ");return m.length>0?{query:h,params:m}:{query:h}}var x=/^[a-zA-Z_][a-zA-Z0-9_]*$/;function E(t){if(!x.test(t))throw new s("sql-invalid",`invalid identifier: ${t}`);return `"${t}"`}function M(t){return typeof t=="object"&&t!==null&&"code"in t}async function R(t,e){let n=Date.now();if(!Number.isFinite(e.timeoutMs)||!Number.isInteger(e.timeoutMs)||e.timeoutMs<=0)throw new s("sql-invalid",`timeoutMs must be a positive integer; got ${String(e.timeoutMs)}`);if(!Number.isFinite(e.rowLimit)||!Number.isInteger(e.rowLimit)||e.rowLimit<=0)throw new s("sql-invalid",`rowLimit must be a positive integer; got ${String(e.rowLimit)}`);let o=typeof e.runAsRole=="string"&&e.runAsRole.length>0?e.runAsRole:void 0;try{let i=await t.begin(async l=>{o!==void 0&&await l.unsafe(`SET LOCAL ROLE ${E(o)}`),await l.unsafe(`SET LOCAL statement_timeout = ${e.timeoutMs}`);let p;if(e.params!=null){let d=[...e.params];for(let a=0;a<d.length;a++){let m=d[a];if(m!==null&&typeof m!="string"&&typeof m!="number"&&typeof m!="boolean")throw new s("sql-invalid",`params[${a}] must be string|number|boolean|null; got ${typeof m}`)}p=d;}return await l.unsafe(e.query,p)}),r=i.length>e.rowLimit,c=i.slice(0,e.rowLimit);return {rows:c,rowCount:c.length,durationMs:Date.now()-n,truncated:r}}catch(i){if(M(i)){if(i.code==="42501")throw new s("sql-permission-denied","permission denied for SQL operation");if(i.code==="57014")throw new s("sql-timeout","statement timeout")}throw i}}var D=/email|phone|ssn|address|dob|password|secret|token|card/i;async function T(t){let e=await t`
225
2
  SELECT table_name
226
3
  FROM information_schema.tables
227
4
  WHERE table_schema = 'public'
228
5
  AND table_type = 'BASE TABLE'
229
6
  ORDER BY table_name
230
- `;
231
- const result = [];
232
- for (const { table_name } of tables) {
233
- const columns = await sql`
7
+ `,n=[];for(let{table_name:o}of e){let i=await t`
234
8
  SELECT
235
9
  c.column_name,
236
10
  c.data_type,
@@ -244,10 +18,9 @@ async function introspect(sql) {
244
18
  ON pgd.objoid = st.relid
245
19
  AND pgd.objsubid = c.ordinal_position
246
20
  WHERE c.table_schema = 'public'
247
- AND c.table_name = ${table_name}
21
+ AND c.table_name = ${o}
248
22
  ORDER BY c.ordinal_position
249
- `;
250
- const pkRows = await sql`
23
+ `,r=await t`
251
24
  SELECT kcu.column_name
252
25
  FROM information_schema.table_constraints tc
253
26
  JOIN information_schema.key_column_usage kcu
@@ -255,11 +28,10 @@ async function introspect(sql) {
255
28
  AND tc.table_schema = kcu.table_schema
256
29
  AND tc.table_name = kcu.table_name
257
30
  WHERE tc.table_schema = 'public'
258
- AND tc.table_name = ${table_name}
31
+ AND tc.table_name = ${o}
259
32
  AND tc.constraint_type = 'PRIMARY KEY'
260
33
  ORDER BY kcu.ordinal_position
261
- `;
262
- const fkRows = await sql`
34
+ `,c=await t`
263
35
  SELECT
264
36
  kcu.column_name,
265
37
  ccu.table_name AS references_table,
@@ -273,328 +45,19 @@ async function introspect(sql) {
273
45
  ON ccu.constraint_name = tc.constraint_name
274
46
  AND ccu.table_schema = tc.table_schema
275
47
  WHERE tc.table_schema = 'public'
276
- AND tc.table_name = ${table_name}
48
+ AND tc.table_name = ${o}
277
49
  AND tc.constraint_type = 'FOREIGN KEY'
278
50
  ORDER BY kcu.ordinal_position
279
- `;
280
- const countRows = await sql`
51
+ `,p=(await t`
281
52
  SELECT reltuples::bigint AS row_count
282
53
  FROM pg_class
283
- WHERE relname = ${table_name}
284
- `;
285
- const rowCountRaw = countRows[0]?.row_count;
286
- const row_count = rowCountRaw === null || rowCountRaw === void 0 ? 0 : Number(rowCountRaw);
287
- result.push({
288
- schema: "public",
289
- name: table_name,
290
- columns: columns.map((col) => {
291
- const column = {
292
- name: col.column_name,
293
- type: col.data_type,
294
- nullable: col.is_nullable === "YES"
295
- };
296
- if (col.comment) {
297
- column.comment = col.comment;
298
- }
299
- if (PII_RE.test(col.column_name)) {
300
- column.isPii = true;
301
- }
302
- return column;
303
- }),
304
- primary_key: pkRows.map((r) => r.column_name),
305
- foreign_keys: fkRows.map((r) => ({
306
- column: r.column_name,
307
- references: {
308
- table: r.references_table,
309
- column: r.references_column
310
- }
311
- })),
312
- row_count
313
- });
314
- }
315
- return result;
316
- }
317
-
318
- // src/setup-role.ts
319
- var SETUP_ROLE_LOCK_KEY = 982374623;
320
- function isDuplicateRoleError(err) {
321
- return err != null && typeof err === "object" && "code" in err && err.code === "42710";
322
- }
323
- async function setupRole(sql, role, options) {
324
- const roleIdent = escapeIdent(role);
325
- await sql.unsafe(`SELECT pg_advisory_lock(${SETUP_ROLE_LOCK_KEY})`);
326
- try {
327
- try {
328
- await sql.unsafe(`CREATE ROLE ${roleIdent} LOGIN`);
329
- } catch (err) {
330
- if (!isDuplicateRoleError(err)) {
331
- throw err;
332
- }
333
- }
334
- await sql.unsafe(`
54
+ WHERE relname = ${o}
55
+ `)[0]?.row_count,d=p==null?0:Number(p);n.push({schema:"public",name:o,columns:i.map(a=>{let m={name:a.column_name,type:a.data_type,nullable:a.is_nullable==="YES"};return a.comment&&(m.comment=a.comment),D.test(a.column_name)&&(m.isPii=true),m}),primary_key:r.map(a=>a.column_name),foreign_keys:c.map(a=>({column:a.column_name,references:{table:a.references_table,column:a.references_column}})),row_count:d});}return n}var y=982374623;function P(t){return t!=null&&typeof t=="object"&&"code"in t&&t.code==="42710"}async function A(t,e,n){let o=E(e);await t.unsafe(`SELECT pg_advisory_lock(${y})`);try{try{await t.unsafe(`CREATE ROLE ${o} LOGIN`);}catch(r){if(!P(r))throw r}await t.unsafe(`
335
56
  CREATE TABLE IF NOT EXISTS arivie_owner_identity (
336
57
  key TEXT PRIMARY KEY,
337
58
  value TEXT NOT NULL
338
59
  );
339
- `);
340
- await sql.unsafe(`GRANT USAGE ON SCHEMA public TO ${roleIdent}`);
341
- const allowedTables = options?.allowedTables;
342
- if (allowedTables && allowedTables.length > 0) {
343
- for (const table of allowedTables) {
344
- await sql.unsafe(
345
- `GRANT SELECT ON TABLE public.${escapeIdent(table)} TO ${roleIdent}`
346
- );
347
- }
348
- } else {
349
- await sql.unsafe(
350
- `GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${roleIdent}`
351
- );
352
- }
353
- await sql.unsafe(
354
- `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${roleIdent}`
355
- );
356
- await sql.unsafe(
357
- `GRANT ${roleIdent} TO CURRENT_USER WITH SET TRUE`
358
- );
359
- } finally {
360
- await sql.unsafe(`SELECT pg_advisory_unlock(${SETUP_ROLE_LOCK_KEY})`);
361
- }
362
- }
363
-
364
- // src/verify.ts
365
- async function verifyOwnerIdentity(sql, expectedOwnerId) {
366
- const rows = await sql`
60
+ `),await t.unsafe(`GRANT USAGE ON SCHEMA public TO ${o}`);let i=n?.allowedTables;if(i&&i.length>0)for(let r of i)await t.unsafe(`GRANT SELECT ON TABLE public.${E(r)} TO ${o}`);else await t.unsafe(`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${o}`);await t.unsafe(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${o}`),await t.unsafe(`GRANT ${o} TO CURRENT_USER WITH SET TRUE`);}finally{await t.unsafe(`SELECT pg_advisory_unlock(${y})`);}}async function O(t,e){let n=await t`
367
61
  SELECT value FROM arivie_owner_identity WHERE key = 'owner_id'
368
- `;
369
- if (rows.length === 0) {
370
- throw new ArivieBoundaryError(
371
- {
372
- reason: "identity-table-missing",
373
- expected: expectedOwnerId
374
- },
375
- "arivie_owner_identity table missing or empty; run 'arivie setup' first"
376
- );
377
- }
378
- const dbValue = rows[0]?.value;
379
- if (dbValue !== expectedOwnerId) {
380
- throw new ArivieBoundaryError(
381
- {
382
- reason: "identity-mismatch",
383
- dbValue,
384
- expected: expectedOwnerId
385
- },
386
- `owner identity mismatch: database has '${String(dbValue)}', expected '${expectedOwnerId}'`
387
- );
388
- }
389
- }
390
-
391
- // src/adapter.ts
392
- function derivePostgresAdapterId(url) {
393
- try {
394
- const parsed = new URL(url);
395
- const host = parsed.hostname || "localhost";
396
- const db = parsed.pathname.replace(/^\//, "") || "postgres";
397
- return `postgres:${host}/${db}`;
398
- } catch {
399
- const hash = createHash("sha256").update(url).digest("hex").slice(0, 12);
400
- return `postgres:${hash}`;
401
- }
402
- }
403
- function postgresAdapter(opts) {
404
- const sql = postgres(opts.url, {
405
- max: opts.maxConnections ?? 10,
406
- idle_timeout: (opts.idleTimeoutMs ?? 3e4) / 1e3,
407
- onnotice: () => {
408
- }
409
- });
410
- return {
411
- kind: "postgres",
412
- id: derivePostgresAdapterId(opts.url),
413
- url: opts.url,
414
- sql,
415
- execute: (executeOpts) => executeImpl(sql, executeOpts),
416
- introspect: () => introspect(sql),
417
- verifyOwnerIdentity: (expectedOwnerId) => verifyOwnerIdentity(sql, expectedOwnerId),
418
- setupRole: (role, options) => setupRole(sql, role, options),
419
- compileMetric: compileMetricForPostgres,
420
- close: async () => {
421
- await sql.end();
422
- }
423
- };
424
- }
425
-
426
- // src/sql-guard.ts
427
- var FORBIDDEN_KEYWORDS = [
428
- "INSERT",
429
- "UPDATE",
430
- "DELETE",
431
- "MERGE",
432
- "TRUNCATE",
433
- "DROP",
434
- "CREATE",
435
- "ALTER",
436
- "GRANT",
437
- "REVOKE",
438
- "REINDEX",
439
- "VACUUM",
440
- "CLUSTER",
441
- "COPY",
442
- "CALL",
443
- "DO",
444
- "LOCK",
445
- "COMMENT",
446
- "REFRESH",
447
- "REASSIGN",
448
- "EXECUTE",
449
- "PREPARE",
450
- "DEALLOCATE",
451
- "DISCARD",
452
- "LISTEN",
453
- "NOTIFY",
454
- "UNLISTEN",
455
- "SET",
456
- "RESET"
457
- ];
458
- var SYSTEM_CATALOG_PATTERN = /\b(pg_catalog|information_schema)\b/i;
459
- var FORBIDDEN_PATTERN = new RegExp(
460
- `\\b(${FORBIDDEN_KEYWORDS.join("|")})\\b`,
461
- "i"
462
- );
463
- function stripLiteralsAndComments(sql) {
464
- const out = [];
465
- let i = 0;
466
- const n = sql.length;
467
- while (i < n) {
468
- const c = sql[i];
469
- if (c === void 0) {
470
- break;
471
- }
472
- const next = i + 1 < n ? sql[i + 1] : "";
473
- if (c === "-" && next === "-") {
474
- while (i < n && sql[i] !== "\n") {
475
- out.push(" ");
476
- i += 1;
477
- }
478
- continue;
479
- }
480
- if (c === "/" && next === "*") {
481
- out.push(" ");
482
- i += 2;
483
- while (i < n) {
484
- if (sql[i] === "*" && i + 1 < n && sql[i + 1] === "/") {
485
- out.push(" ");
486
- i += 2;
487
- break;
488
- }
489
- out.push(" ");
490
- i += 1;
491
- }
492
- continue;
493
- }
494
- if (c === "'") {
495
- out.push("'");
496
- i += 1;
497
- while (i < n) {
498
- if (sql[i] === "'") {
499
- if (i + 1 < n && sql[i + 1] === "'") {
500
- out.push(" ");
501
- i += 2;
502
- continue;
503
- }
504
- out.push("'");
505
- i += 1;
506
- break;
507
- }
508
- out.push(" ");
509
- i += 1;
510
- }
511
- continue;
512
- }
513
- if (c === '"') {
514
- out.push('"');
515
- i += 1;
516
- while (i < n) {
517
- if (sql[i] === '"') {
518
- if (i + 1 < n && sql[i + 1] === '"') {
519
- out.push(" ");
520
- i += 2;
521
- continue;
522
- }
523
- out.push('"');
524
- i += 1;
525
- break;
526
- }
527
- out.push(" ");
528
- i += 1;
529
- }
530
- continue;
531
- }
532
- if (c === "$") {
533
- const tagMatch = /^\$([A-Za-z_][A-Za-z_0-9]*)?\$/.exec(sql.slice(i));
534
- if (tagMatch != null) {
535
- const tag = tagMatch[0];
536
- out.push(" ".repeat(tag.length));
537
- i += tag.length;
538
- const end = sql.indexOf(tag, i);
539
- if (end === -1) {
540
- while (i < n) {
541
- out.push(" ");
542
- i += 1;
543
- }
544
- continue;
545
- }
546
- while (i < end) {
547
- out.push(" ");
548
- i += 1;
549
- }
550
- out.push(" ".repeat(tag.length));
551
- i += tag.length;
552
- continue;
553
- }
554
- }
555
- out.push(c);
556
- i += 1;
557
- }
558
- return out.join("");
559
- }
560
- function firstKeyword(sql) {
561
- const stripped = stripLiteralsAndComments(sql);
562
- const m = /\s*\(*\s*([A-Za-z_][A-Za-z_0-9]*)/.exec(stripped);
563
- return m?.[1] ? m[1].toUpperCase() : null;
564
- }
565
- function validateExecuteSql(sql) {
566
- const trimmed = sql.trim();
567
- if (trimmed.length === 0) {
568
- throw new ToolError("sql-invalid", "empty query");
569
- }
570
- const stripped = stripLiteralsAndComments(trimmed);
571
- if (stripped.includes(";")) {
572
- const lastSemi = stripped.lastIndexOf(";");
573
- const tail = stripped.slice(lastSemi + 1).trim();
574
- if (tail.length > 0) {
575
- throw new ToolError(
576
- "sql-invalid",
577
- "multi-statement queries are not allowed"
578
- );
579
- }
580
- }
581
- const head = firstKeyword(trimmed);
582
- if (head !== "SELECT" && head !== "WITH") {
583
- throw new ToolError(
584
- "sql-invalid",
585
- "only SELECT and WITH statements are allowed"
586
- );
587
- }
588
- if (SYSTEM_CATALOG_PATTERN.test(stripped)) {
589
- throw new ToolError("sql-blocked", "system catalog access is blocked");
590
- }
591
- const forbidden = FORBIDDEN_PATTERN.exec(stripped);
592
- if (forbidden != null) {
593
- throw new ToolError(
594
- "sql-blocked",
595
- `forbidden keyword '${forbidden[1]?.toUpperCase()}' in query`
596
- );
597
- }
598
- }
599
-
600
- export { ToolError, compileMetricForPostgres, postgresAdapter, validateExecuteSql };
62
+ `;if(n.length===0)throw new ArivieBoundaryError({reason:"identity-table-missing",expected:e},"arivie_owner_identity table missing or empty; run 'arivie setup' first");let o=n[0]?.value;if(o!==e)throw new ArivieBoundaryError({reason:"identity-mismatch",dbValue:o,expected:e},`owner identity mismatch: database has '${String(o)}', expected '${e}'`)}function U(t){try{let e=new URL(t),n=e.hostname||"localhost",o=e.pathname.replace(/^\//,"")||"postgres";return `postgres:${n}/${o}`}catch{return `postgres:${createHash("sha256").update(t).digest("hex").slice(0,12)}`}}function Y(t){let e=F(t.url,{max:t.maxConnections??10,idle_timeout:(t.idleTimeoutMs??3e4)/1e3,onnotice:()=>{}});return {kind:"postgres",id:U(t.url),url:t.url,sql:e,execute:n=>R(e,n),introspect:()=>T(e),verifyOwnerIdentity:n=>O(e,n),setupRole:(n,o)=>A(e,n,o),compileMetric:b,close:async()=>{await e.end();}}}var B=["INSERT","UPDATE","DELETE","MERGE","TRUNCATE","DROP","CREATE","ALTER","GRANT","REVOKE","REINDEX","VACUUM","CLUSTER","COPY","CALL","DO","LOCK","COMMENT","REFRESH","REASSIGN","EXECUTE","PREPARE","DEALLOCATE","DISCARD","LISTEN","NOTIFY","UNLISTEN","SET","RESET"],H=/\b(pg_catalog|information_schema)\b/i,z=new RegExp(`\\b(${B.join("|")})\\b`,"i");function N(t){let e=[],n=0,o=t.length;for(;n<o;){let i=t[n];if(i===void 0)break;let r=n+1<o?t[n+1]:"";if(i==="-"&&r==="-"){for(;n<o&&t[n]!==`
63
+ `;)e.push(" "),n+=1;continue}if(i==="/"&&r==="*"){for(e.push(" "),n+=2;n<o;){if(t[n]==="*"&&n+1<o&&t[n+1]==="/"){e.push(" "),n+=2;break}e.push(" "),n+=1;}continue}if(i==="'"){for(e.push("'"),n+=1;n<o;){if(t[n]==="'"){if(n+1<o&&t[n+1]==="'"){e.push(" "),n+=2;continue}e.push("'"),n+=1;break}e.push(" "),n+=1;}continue}if(i==='"'){for(e.push('"'),n+=1;n<o;){if(t[n]==='"'){if(n+1<o&&t[n+1]==='"'){e.push(" "),n+=2;continue}e.push('"'),n+=1;break}e.push(" "),n+=1;}continue}if(i==="$"){let c=/^\$([A-Za-z_][A-Za-z_0-9]*)?\$/.exec(t.slice(n));if(c!=null){let l=c[0];e.push(" ".repeat(l.length)),n+=l.length;let p=t.indexOf(l,n);if(p===-1){for(;n<o;)e.push(" "),n+=1;continue}for(;n<p;)e.push(" "),n+=1;e.push(" ".repeat(l.length)),n+=l.length;continue}}e.push(i),n+=1;}return e.join("")}function G(t){let e=N(t),n=/\s*\(*\s*([A-Za-z_][A-Za-z_0-9]*)/.exec(e);return n?.[1]?n[1].toUpperCase():null}function Z(t){let e=t.trim();if(e.length===0)throw new s("sql-invalid","empty query");let n=N(e);if(n.includes(";")){let r=n.lastIndexOf(";");if(n.slice(r+1).trim().length>0)throw new s("sql-invalid","multi-statement queries are not allowed")}let o=G(e);if(o!=="SELECT"&&o!=="WITH")throw new s("sql-invalid","only SELECT and WITH statements are allowed");if(H.test(n))throw new s("sql-blocked","system catalog access is blocked");let i=z.exec(n);if(i!=null)throw new s("sql-blocked",`forbidden keyword '${i[1]?.toUpperCase()}' in query`)}export{s as ToolError,b as compileMetricForPostgres,Y as postgresAdapter,Z as validateExecuteSql};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arivie/db-postgres",
3
- "version": "0.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "Arivie Postgres adapter — role-scoped execute, introspect, owner-identity verification.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -19,7 +19,7 @@
19
19
  "CHANGELOG.md"
20
20
  ],
21
21
  "dependencies": {
22
- "@arivie/core": "1.0.0"
22
+ "@arivie/core": "2.0.0"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "postgres": "^3.4.9"
@@ -32,7 +32,7 @@
32
32
  "tsup": "^8.5.1",
33
33
  "typescript": "^6.0.0",
34
34
  "vitest": "^4.1.0",
35
- "@arivie/semantic": "0.1.0"
35
+ "@arivie/semantic": "2.0.0"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public",