@m1212e/rumble 0.24.2 → 0.25.1
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/out/client.cjs +54 -50
- package/out/client.cjs.map +1 -1
- package/out/client.d.cts.map +1 -1
- package/out/client.d.mts.map +1 -1
- package/out/client.mjs +54 -50
- package/out/client.mjs.map +1 -1
- package/out/index.cjs +803 -473
- package/out/index.cjs.map +1 -1
- package/out/index.d.cts +272 -284
- package/out/index.d.cts.map +1 -1
- package/out/index.d.mts +268 -280
- package/out/index.d.mts.map +1 -1
- package/out/index.mjs +808 -478
- package/out/index.mjs.map +1 -1
- package/package.json +9 -8
package/out/index.mjs
CHANGED
|
@@ -7,15 +7,15 @@ import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspect
|
|
|
7
7
|
import { SpanStatusCode, isSpanContextValid, trace } from "@opentelemetry/api";
|
|
8
8
|
import { capitalize, cloneDeep, debounce, merge, toMerged } from "es-toolkit";
|
|
9
9
|
import { createPubSub, createYoga } from "graphql-yoga";
|
|
10
|
-
import { One, count, getTableColumns, getTableName, is, isTable, relationsFilterToSQL, sql } from "drizzle-orm";
|
|
10
|
+
import { One, aliasedTable, count, getTableColumns, getTableName, is, isTable, or, relationsFilterToSQL, sql } from "drizzle-orm";
|
|
11
11
|
import { ATTR_GRAPHQL_DOCUMENT, ATTR_GRAPHQL_OPERATION_NAME, ATTR_GRAPHQL_OPERATION_TYPE } from "@opentelemetry/semantic-conventions/incubating";
|
|
12
12
|
import { AttributeNames, SpanNames, createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
|
|
13
13
|
import { toCamelCase } from "drizzle-orm/casing";
|
|
14
|
-
import { PgColumn, PgEnumColumn, PgEnumObjectColumn, PgTable, isPgEnum } from "drizzle-orm/pg-core";
|
|
15
|
-
import { MySqlEnumColumn, MySqlEnumObjectColumn, MySqlTable } from "drizzle-orm/mysql-core";
|
|
14
|
+
import { PgColumn, PgEnumColumn, PgEnumObjectColumn, PgTable, getTableConfig, isPgEnum } from "drizzle-orm/pg-core";
|
|
15
|
+
import { MySqlEnumColumn, MySqlEnumObjectColumn, MySqlTable, getTableConfig as getTableConfig$1 } from "drizzle-orm/mysql-core";
|
|
16
16
|
import pluralize from "pluralize";
|
|
17
|
-
import { SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
18
|
-
import SchemaBuilder, { BasePlugin } from "@pothos/core";
|
|
17
|
+
import { SQLiteTable, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
|
|
18
|
+
import SchemaBuilder, { BasePlugin, isThenable } from "@pothos/core";
|
|
19
19
|
import DrizzlePlugin from "@pothos/plugin-drizzle";
|
|
20
20
|
import SmartSubscriptionsPlugin, { subscribeOptionsFromIterator } from "@pothos/plugin-smart-subscriptions";
|
|
21
21
|
import TracingPlugin, { isRootField, wrapResolver } from "@pothos/plugin-tracing";
|
|
@@ -186,38 +186,76 @@ function mapNullFieldsToUndefined(obj) {
|
|
|
186
186
|
}
|
|
187
187
|
//#endregion
|
|
188
188
|
//#region package.json
|
|
189
|
-
var version = "0.
|
|
189
|
+
var version = "0.25.1";
|
|
190
190
|
//#endregion
|
|
191
|
-
//#region lib/helpers/
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
191
|
+
//#region lib/helpers/tableHelpers.ts
|
|
192
|
+
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
193
|
+
function buildIndex(db) {
|
|
194
|
+
const byTsName = /* @__PURE__ */ new Map();
|
|
195
|
+
const byDbName = /* @__PURE__ */ new Map();
|
|
196
|
+
const byTableRef = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const [tsName, rawEntry] of Object.entries(db._.relations ?? {})) {
|
|
198
|
+
const entry = rawEntry;
|
|
199
|
+
const tableObj = entry.table;
|
|
200
|
+
if (!tableObj) continue;
|
|
201
|
+
const columns = isTable(tableObj) ? getTableColumns(tableObj) : {};
|
|
202
|
+
const primaryKey = Object.fromEntries(Object.entries(columns).filter(([, col]) => col.primary));
|
|
203
|
+
const dbName = isTable(tableObj) ? getTableName(tableObj) : entry.name ?? tsName;
|
|
204
|
+
const resolved = {
|
|
205
|
+
name: entry.name ?? tsName,
|
|
206
|
+
dbName,
|
|
207
|
+
table: tableObj,
|
|
208
|
+
columns,
|
|
209
|
+
primaryKey,
|
|
210
|
+
relations: entry.relations ?? {},
|
|
211
|
+
isTable: isTable(tableObj),
|
|
212
|
+
foundRelation: entry
|
|
204
213
|
};
|
|
214
|
+
byTsName.set(resolved.name, resolved);
|
|
215
|
+
byDbName.set(resolved.dbName, resolved);
|
|
216
|
+
byTableRef.set(tableObj, resolved);
|
|
205
217
|
}
|
|
206
218
|
return {
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
byTsName,
|
|
220
|
+
byDbName,
|
|
221
|
+
byTableRef
|
|
209
222
|
};
|
|
210
223
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return
|
|
224
|
+
function getIndex(db) {
|
|
225
|
+
let idx = indexCache.get(db);
|
|
226
|
+
if (!idx) {
|
|
227
|
+
idx = buildIndex(db);
|
|
228
|
+
indexCache.set(db, idx);
|
|
229
|
+
}
|
|
230
|
+
return idx;
|
|
218
231
|
}
|
|
219
|
-
function
|
|
220
|
-
|
|
232
|
+
function tableHelper({ db, table }) {
|
|
233
|
+
const idx = getIndex(db);
|
|
234
|
+
let entry;
|
|
235
|
+
if (typeof table === "string") entry = idx.byTsName.get(table) ?? idx.byDbName.get(table);
|
|
236
|
+
else if (table && typeof table === "object") {
|
|
237
|
+
const t = table;
|
|
238
|
+
if (t.table) {
|
|
239
|
+
entry = idx.byTableRef.get(t.table);
|
|
240
|
+
if (!entry && t.name) entry = idx.byTsName.get(t.name) ?? idx.byDbName.get(t.name);
|
|
241
|
+
}
|
|
242
|
+
if (!entry) entry = idx.byTableRef.get(t);
|
|
243
|
+
if (!entry && isTable(t)) {
|
|
244
|
+
const dbName = getTableName(t);
|
|
245
|
+
entry = idx.byDbName.get(dbName);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!entry) throw new RumbleError(`Could not find schema for ${JSON.stringify(typeof table === "string" ? table : "<table-object>")}`);
|
|
249
|
+
return {
|
|
250
|
+
columns: entry.columns,
|
|
251
|
+
primaryKey: entry.primaryKey,
|
|
252
|
+
relations: entry.relations,
|
|
253
|
+
dbName: entry.dbName,
|
|
254
|
+
tsName: entry.name,
|
|
255
|
+
table: entry.table,
|
|
256
|
+
isTable: entry.isTable,
|
|
257
|
+
foundRelation: entry.foundRelation
|
|
258
|
+
};
|
|
221
259
|
}
|
|
222
260
|
//#endregion
|
|
223
261
|
//#region lib/helpers/mergeFilters.ts
|
|
@@ -232,20 +270,36 @@ const EmptyFilter$1 = Symbol.for("drizzle:EmptyFilter");
|
|
|
232
270
|
function realWhere(where) {
|
|
233
271
|
return where === EmptyFilter$1 ? void 0 : where;
|
|
234
272
|
}
|
|
273
|
+
function mergeWhere(a, b, mode) {
|
|
274
|
+
if (a && b) return mode === "OR" ? { OR: [a, b] } : { AND: [a, b] };
|
|
275
|
+
return mode === "OR" ? void 0 : a ?? b;
|
|
276
|
+
}
|
|
277
|
+
function mergeColumns(a, b) {
|
|
278
|
+
if (!a && !b) return void 0;
|
|
279
|
+
const result = {};
|
|
280
|
+
for (const [key, value] of [...Object.entries(a ?? {}), ...Object.entries(b ?? {})]) if (value === true) result[key] = true;
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function mergeRecords(a, b) {
|
|
284
|
+
return a || b ? toMerged(a ?? {}, b ?? {}) : void 0;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Combines two limit/offset style bounds. For OR the looser bound wins (and a
|
|
288
|
+
* missing side means unbounded), for AND the stricter bound wins.
|
|
289
|
+
*/
|
|
290
|
+
function mergeBound(a, b, mode, orPick) {
|
|
291
|
+
if (mode === "OR") return a === void 0 || b === void 0 ? void 0 : orPick(a, b);
|
|
292
|
+
return a || b ? Math.min(a ?? Infinity, b ?? Infinity) : void 0;
|
|
293
|
+
}
|
|
235
294
|
function mergeFilters(filterA, filterB, mode = "AND") {
|
|
236
|
-
const filterAWhere = realWhere(filterA?.where);
|
|
237
|
-
const filterBWhere = realWhere(filterB?.where);
|
|
238
295
|
return {
|
|
239
|
-
where:
|
|
240
|
-
columns:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
limit: mode === "OR" ? filterA?.limit === void 0 || filterB?.limit === void 0 ? void 0 : Math.max(filterA.limit, filterB.limit) : filterA?.limit || filterB?.limit ? Math.min(filterA?.limit ?? Infinity, filterB?.limit ?? Infinity) : void 0,
|
|
247
|
-
offset: mode === "OR" ? filterA?.offset === void 0 || filterB?.offset === void 0 ? void 0 : Math.min(filterA.offset, filterB.offset) : filterA?.offset || filterB?.offset ? Math.min(filterA?.offset ?? Infinity, filterB?.offset ?? Infinity) : void 0,
|
|
248
|
-
with: filterA?.with || filterB?.with ? toMerged(filterA?.with ?? {}, filterB?.with ?? {}) : void 0
|
|
296
|
+
where: mergeWhere(realWhere(filterA?.where), realWhere(filterB?.where), mode),
|
|
297
|
+
columns: mergeColumns(filterA?.columns, filterB?.columns),
|
|
298
|
+
extras: mergeRecords(filterA?.extras, filterB?.extras),
|
|
299
|
+
orderBy: mergeRecords(filterA?.orderBy, filterB?.orderBy),
|
|
300
|
+
limit: mergeBound(filterA?.limit, filterB?.limit, mode, Math.max),
|
|
301
|
+
offset: mergeBound(filterA?.offset, filterB?.offset, mode, Math.min),
|
|
302
|
+
with: mergeRecords(filterA?.with, filterB?.with)
|
|
249
303
|
};
|
|
250
304
|
}
|
|
251
305
|
//#endregion
|
|
@@ -263,6 +317,201 @@ function sanitizeFilterValue(value) {
|
|
|
263
317
|
return value;
|
|
264
318
|
}
|
|
265
319
|
//#endregion
|
|
320
|
+
//#region lib/abilityBuilder/keys.ts
|
|
321
|
+
const ACTION_FLAG_KEY = "__rumble_action";
|
|
322
|
+
const columnFlagKey = (group) => `__rumble_columns_${group}`;
|
|
323
|
+
const columnMaskKey = Symbol.for("rumble:columnMask");
|
|
324
|
+
const resolvedFilterKey = Symbol.for("rumble:resolvedFilter");
|
|
325
|
+
const requestStateKey = Symbol.for("rumble:requestState");
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region lib/abilityBuilder/requestState.ts
|
|
328
|
+
function createRequestState() {
|
|
329
|
+
return {
|
|
330
|
+
masked: /* @__PURE__ */ new WeakMap(),
|
|
331
|
+
actions: /* @__PURE__ */ new Map(),
|
|
332
|
+
filterLoaders: /* @__PURE__ */ new Map()
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function attachRequestState(abilities, state) {
|
|
336
|
+
Object.defineProperty(abilities, requestStateKey, { value: state });
|
|
337
|
+
}
|
|
338
|
+
function requestStateOf(abilities) {
|
|
339
|
+
return abilities?.[requestStateKey];
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region lib/abilityBuilder/columnMask.ts
|
|
343
|
+
function referencesRelation(filter, relations) {
|
|
344
|
+
if (!filter || typeof filter !== "object") return false;
|
|
345
|
+
return Object.entries(filter).some(([key, value]) => {
|
|
346
|
+
if (key === "AND" || key === "OR") return value.some((v) => referencesRelation(v, relations));
|
|
347
|
+
if (key === "NOT") return referencesRelation(value, relations);
|
|
348
|
+
return key in relations;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function selectedColumns(columns, allColumns) {
|
|
352
|
+
if (!columns) return new Set(allColumns);
|
|
353
|
+
const entries = Object.entries(columns).filter(([, v]) => v !== void 0);
|
|
354
|
+
if (entries.some(([, v]) => v)) return new Set(entries.filter(([, v]) => v).map(([k]) => k));
|
|
355
|
+
if (entries.length === 0) return /* @__PURE__ */ new Set();
|
|
356
|
+
return new Set(allColumns.filter((c) => columns[c] !== false));
|
|
357
|
+
}
|
|
358
|
+
function hiddenColumns(mask, matched, allColumns, primaryKey) {
|
|
359
|
+
let hidden = mask.hiddenPerMatch.get(matched);
|
|
360
|
+
if (!hidden) {
|
|
361
|
+
const columns = allColumns.filter((column) => !mask.guaranteed.has(column) && !mask.conditional.some((columns, group) => matched[group] === "1" && columns.has(column)));
|
|
362
|
+
hidden = {
|
|
363
|
+
cleared: columns.filter((c) => !primaryKey.includes(c)),
|
|
364
|
+
primaryKey: columns.some((c) => primaryKey.includes(c))
|
|
365
|
+
};
|
|
366
|
+
mask.hiddenPerMatch.set(matched, hidden);
|
|
367
|
+
}
|
|
368
|
+
return hidden;
|
|
369
|
+
}
|
|
370
|
+
const hasWhere = (where) => !!where && Object.keys(where).length > 0;
|
|
371
|
+
/** Columns every returned row is allowed to expose. */
|
|
372
|
+
function guaranteedColumns(rules, allColumnNames) {
|
|
373
|
+
const guaranteed = /* @__PURE__ */ new Set();
|
|
374
|
+
for (const rule of rules) {
|
|
375
|
+
if (hasWhere(rule.where)) continue;
|
|
376
|
+
for (const column of rule.columns) guaranteed.add(column);
|
|
377
|
+
}
|
|
378
|
+
for (const column of allColumnNames) if (rules.every((r) => r.columns.has(column))) guaranteed.add(column);
|
|
379
|
+
return guaranteed;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Groups the conditional rules by the columns they grant on top of the
|
|
383
|
+
* guaranteed ones. Each group gets its own flag.
|
|
384
|
+
*/
|
|
385
|
+
function conditionalGroups(rules, guaranteed) {
|
|
386
|
+
const groups = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const rule of rules) {
|
|
388
|
+
if (!hasWhere(rule.where)) continue;
|
|
389
|
+
const additional = [...rule.columns].filter((c) => !guaranteed.has(c)).sort();
|
|
390
|
+
if (additional.length === 0) continue;
|
|
391
|
+
const key = additional.join(",");
|
|
392
|
+
const group = groups.get(key);
|
|
393
|
+
if (group) group.wheres.push(rule.where);
|
|
394
|
+
else groups.set(key, {
|
|
395
|
+
columns: new Set(additional),
|
|
396
|
+
wheres: [rule.where]
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return [...groups.values()];
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Creates the resolver which turns the abilities of a table into the columns
|
|
403
|
+
* to select and, if columns depend on the matched ability, extras flagging
|
|
404
|
+
* per row which abilities matched plus the mask to decode them.
|
|
405
|
+
*/
|
|
406
|
+
function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
407
|
+
const allColumnNames = Object.keys(tableSchema.columns);
|
|
408
|
+
const primaryKeyNames = Object.keys(tableSchema.primaryKey);
|
|
409
|
+
const primaryKeyName = primaryKeyNames[0];
|
|
410
|
+
const toSQL = (table, wheres) => or(...wheres.map((where) => relationsFilterToSQL(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)));
|
|
411
|
+
const uncorrelated = (table, wheres, group) => {
|
|
412
|
+
const inner = aliasedTable(tableSchema.table, `rumble_columns_${group}`);
|
|
413
|
+
return sql`${table[primaryKeyName]} in (${db.select({ pk: inner[primaryKeyName] }).from(inner).where(toSQL(inner, wheres))})`;
|
|
414
|
+
};
|
|
415
|
+
return function resolveColumnAccess(action, filters) {
|
|
416
|
+
const rules = filters.map((f) => ({
|
|
417
|
+
where: realWhere(f?.where),
|
|
418
|
+
columns: selectedColumns(f?.columns, allColumnNames)
|
|
419
|
+
}));
|
|
420
|
+
const guaranteed = guaranteedColumns(rules, allColumnNames);
|
|
421
|
+
const groups = conditionalGroups(rules, guaranteed);
|
|
422
|
+
const selected = /* @__PURE__ */ new Set([...primaryKeyNames, ...rules.flatMap((r) => [...r.columns])]);
|
|
423
|
+
const columns = selected.size === allColumnNames.length ? void 0 : Object.fromEntries([...selected].map((c) => [c, true]));
|
|
424
|
+
if (guaranteed.size === allColumnNames.length) return {
|
|
425
|
+
columns,
|
|
426
|
+
extras: void 0,
|
|
427
|
+
mask: void 0
|
|
428
|
+
};
|
|
429
|
+
const mask = {
|
|
430
|
+
guaranteed,
|
|
431
|
+
conditional: groups.map((g) => g.columns),
|
|
432
|
+
flags: groups.map((_, group) => columnFlagKey(group)),
|
|
433
|
+
hiddenPerMatch: /* @__PURE__ */ new Map()
|
|
434
|
+
};
|
|
435
|
+
const actionIndex = sql.raw(String(actions.indexOf(action)));
|
|
436
|
+
const extras = { [ACTION_FLAG_KEY]: () => sql`${actionIndex}` };
|
|
437
|
+
groups.forEach(({ wheres }, group) => {
|
|
438
|
+
const relational = wheres.some((w) => referencesRelation(w, tableSchema.relations));
|
|
439
|
+
extras[mask.flags[group]] = (table) => {
|
|
440
|
+
const condition = relational ? uncorrelated(table, wheres, group) : toSQL(table, wheres);
|
|
441
|
+
return sql`case when ${condition} then 1 else 0 end`;
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
columns,
|
|
446
|
+
extras,
|
|
447
|
+
mask
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const isRow = (row) => !!row && typeof row === "object";
|
|
452
|
+
function clearColumns(row, columns) {
|
|
453
|
+
for (let i = 0; i < columns.length; i++) {
|
|
454
|
+
const column = columns[i];
|
|
455
|
+
if (row[column] !== void 0) row[column] = void 0;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function matchedGroups(row, flags) {
|
|
459
|
+
let matched = "";
|
|
460
|
+
for (let i = 0; i < flags.length; i++) matched += Number(row[flags[i]]) === 1 ? "1" : "0";
|
|
461
|
+
return matched;
|
|
462
|
+
}
|
|
463
|
+
function maskRow(row, mask, hidden) {
|
|
464
|
+
if (row["__rumble_action"] !== void 0) row[ACTION_FLAG_KEY] = void 0;
|
|
465
|
+
clearColumns(row, mask.flags);
|
|
466
|
+
clearColumns(row, hidden.cleared);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Creates the function hiding the columns of queried rows which the ability
|
|
470
|
+
* that matched the row does not grant.
|
|
471
|
+
*/
|
|
472
|
+
function createColumnMasker({ actions }, tables) {
|
|
473
|
+
const stateOf = (abilities) => requestStateOf(abilities);
|
|
474
|
+
const maskedActions = (abilities, table) => {
|
|
475
|
+
const { actions } = stateOf(abilities);
|
|
476
|
+
let used = actions.get(table);
|
|
477
|
+
if (!used) {
|
|
478
|
+
used = /* @__PURE__ */ new Set();
|
|
479
|
+
actions.set(table, used);
|
|
480
|
+
}
|
|
481
|
+
return used;
|
|
482
|
+
};
|
|
483
|
+
const primaryKeyHidden = (abilities, row) => stateOf(abilities).masked.get(row) === true;
|
|
484
|
+
const maskColumns = ({ table, action, abilities, entities }, start = 0) => {
|
|
485
|
+
const tableAbilities = abilities[table];
|
|
486
|
+
const { columns: columnNames, primaryKey } = tables.get(table);
|
|
487
|
+
const state = stateOf(abilities);
|
|
488
|
+
const used = maskedActions(abilities, table);
|
|
489
|
+
for (let i = start; i < entities.length; i++) {
|
|
490
|
+
const row = entities[i];
|
|
491
|
+
if (!isRow(row) || state.masked.has(row)) continue;
|
|
492
|
+
const rowAction = row["__rumble_action"] != null ? actions[Number(row[ACTION_FLAG_KEY])] : action;
|
|
493
|
+
used.add(rowAction);
|
|
494
|
+
const resolved = tableAbilities[resolvedFilterKey](rowAction);
|
|
495
|
+
if (!resolved) return tableAbilities.filter(rowAction).then(() => maskColumns({
|
|
496
|
+
table,
|
|
497
|
+
action,
|
|
498
|
+
abilities,
|
|
499
|
+
entities
|
|
500
|
+
}, i));
|
|
501
|
+
const mask = resolved[columnMaskKey];
|
|
502
|
+
if (!mask) continue;
|
|
503
|
+
const hidden = hiddenColumns(mask, matchedGroups(row, mask.flags), columnNames, primaryKey);
|
|
504
|
+
maskRow(row, mask, hidden);
|
|
505
|
+
state.masked.set(row, hidden.primaryKey);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
return {
|
|
509
|
+
maskColumns,
|
|
510
|
+
maskedActions,
|
|
511
|
+
primaryKeyHidden
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
266
515
|
//#region lib/helpers/sqlTypes/types.ts
|
|
267
516
|
/**
|
|
268
517
|
* Strips parameter suffixes like `(256)`, `(10,2)` and timezone modifiers
|
|
@@ -408,76 +657,6 @@ function createDistinctValuesFromSQLType(sqlType) {
|
|
|
408
657
|
throw UnknownTypeRumbleError(sqlType, "Distinct");
|
|
409
658
|
}
|
|
410
659
|
//#endregion
|
|
411
|
-
//#region lib/helpers/tableHelpers.ts
|
|
412
|
-
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
413
|
-
function buildIndex(db) {
|
|
414
|
-
const byTsName = /* @__PURE__ */ new Map();
|
|
415
|
-
const byDbName = /* @__PURE__ */ new Map();
|
|
416
|
-
const byTableRef = /* @__PURE__ */ new Map();
|
|
417
|
-
for (const [tsName, rawEntry] of Object.entries(db._.relations ?? {})) {
|
|
418
|
-
const entry = rawEntry;
|
|
419
|
-
const tableObj = entry.table;
|
|
420
|
-
if (!tableObj) continue;
|
|
421
|
-
const columns = isTable(tableObj) ? getTableColumns(tableObj) : {};
|
|
422
|
-
const primaryKey = Object.fromEntries(Object.entries(columns).filter(([, col]) => col.primary));
|
|
423
|
-
const dbName = isTable(tableObj) ? getTableName(tableObj) : entry.name ?? tsName;
|
|
424
|
-
const resolved = {
|
|
425
|
-
name: entry.name ?? tsName,
|
|
426
|
-
dbName,
|
|
427
|
-
table: tableObj,
|
|
428
|
-
columns,
|
|
429
|
-
primaryKey,
|
|
430
|
-
relations: entry.relations ?? {},
|
|
431
|
-
isTable: isTable(tableObj),
|
|
432
|
-
foundRelation: entry
|
|
433
|
-
};
|
|
434
|
-
byTsName.set(resolved.name, resolved);
|
|
435
|
-
byDbName.set(resolved.dbName, resolved);
|
|
436
|
-
byTableRef.set(tableObj, resolved);
|
|
437
|
-
}
|
|
438
|
-
return {
|
|
439
|
-
byTsName,
|
|
440
|
-
byDbName,
|
|
441
|
-
byTableRef
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
function getIndex(db) {
|
|
445
|
-
let idx = indexCache.get(db);
|
|
446
|
-
if (!idx) {
|
|
447
|
-
idx = buildIndex(db);
|
|
448
|
-
indexCache.set(db, idx);
|
|
449
|
-
}
|
|
450
|
-
return idx;
|
|
451
|
-
}
|
|
452
|
-
function tableHelper({ db, table }) {
|
|
453
|
-
const idx = getIndex(db);
|
|
454
|
-
let entry;
|
|
455
|
-
if (typeof table === "string") entry = idx.byTsName.get(table) ?? idx.byDbName.get(table);
|
|
456
|
-
else if (table && typeof table === "object") {
|
|
457
|
-
const t = table;
|
|
458
|
-
if (t.table) {
|
|
459
|
-
entry = idx.byTableRef.get(t.table);
|
|
460
|
-
if (!entry && t.name) entry = idx.byTsName.get(t.name) ?? idx.byDbName.get(t.name);
|
|
461
|
-
}
|
|
462
|
-
if (!entry) entry = idx.byTableRef.get(t);
|
|
463
|
-
if (!entry && isTable(t)) {
|
|
464
|
-
const dbName = getTableName(t);
|
|
465
|
-
entry = idx.byDbName.get(dbName);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (!entry) throw new RumbleError(`Could not find schema for ${JSON.stringify(typeof table === "string" ? table : "<table-object>")}`);
|
|
469
|
-
return {
|
|
470
|
-
columns: entry.columns,
|
|
471
|
-
primaryKey: entry.primaryKey,
|
|
472
|
-
relations: entry.relations,
|
|
473
|
-
dbName: entry.dbName,
|
|
474
|
-
tsName: entry.name,
|
|
475
|
-
table: entry.table,
|
|
476
|
-
isTable: entry.isTable,
|
|
477
|
-
foundRelation: entry.foundRelation
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
//#endregion
|
|
481
660
|
//#region lib/helpers/telemetry.ts
|
|
482
661
|
const SPAN_EXECUTE = SpanNames.EXECUTE;
|
|
483
662
|
/** @pothos/tracing-opentelemetry's SpanNames enum has no subscribe member. */
|
|
@@ -653,13 +832,161 @@ function durationMs(start) {
|
|
|
653
832
|
return Math.round((performance.now() - start) * 100) / 100;
|
|
654
833
|
}
|
|
655
834
|
//#endregion
|
|
656
|
-
//#region lib/abilityBuilder.ts
|
|
835
|
+
//#region lib/abilityBuilder/filterResponse.ts
|
|
836
|
+
/**
|
|
837
|
+
* The limit of a find many call. An injected merge limit wins unless the
|
|
838
|
+
* ability limit is lower, otherwise the default limit caps the ability limit.
|
|
839
|
+
*/
|
|
840
|
+
function resolveLimit(abilityLimit, mergedLimit, defaultLimit) {
|
|
841
|
+
if (mergedLimit !== void 0 && (!abilityLimit || abilityLimit > mergedLimit)) return mergedLimit;
|
|
842
|
+
if (defaultLimit && (abilityLimit === void 0 || abilityLimit > defaultLimit)) return defaultLimit;
|
|
843
|
+
return abilityLimit ?? void 0;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Creates the function packing resolved query filters into the object handed
|
|
847
|
+
* out by `abilities.<table>.filter(...)`.
|
|
848
|
+
*/
|
|
849
|
+
function createFilterResponder({ db, defaultLimit }, tableSchema) {
|
|
850
|
+
const internalTransformer = (filters, mergedLimit) => {
|
|
851
|
+
const where = filters?.where;
|
|
852
|
+
const extras = filters?.extras;
|
|
853
|
+
const columns = filters?.columns;
|
|
854
|
+
const limit = lazy(() => resolveLimit(filters?.limit, mergedLimit, defaultLimit));
|
|
855
|
+
const sqlTransformedWhere = lazy(() => {
|
|
856
|
+
const casing = db._.session?.dialect?.casing;
|
|
857
|
+
return where ? relationsFilterToSQL(tableSchema.foundRelation.table, sanitizeFilterValue(where), tableSchema.relations, db._.relations, casing) : void 0;
|
|
858
|
+
});
|
|
859
|
+
return {
|
|
860
|
+
query: {
|
|
861
|
+
single: columns ? {
|
|
862
|
+
extras,
|
|
863
|
+
where: sanitizeFilterValue(where),
|
|
864
|
+
columns
|
|
865
|
+
} : {
|
|
866
|
+
extras,
|
|
867
|
+
where: sanitizeFilterValue(where)
|
|
868
|
+
},
|
|
869
|
+
many: columns ? {
|
|
870
|
+
extras,
|
|
871
|
+
where: sanitizeFilterValue(where),
|
|
872
|
+
columns,
|
|
873
|
+
get limit() {
|
|
874
|
+
return limit();
|
|
875
|
+
}
|
|
876
|
+
} : {
|
|
877
|
+
extras,
|
|
878
|
+
where: sanitizeFilterValue(where),
|
|
879
|
+
get limit() {
|
|
880
|
+
return limit();
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
sql: { get where() {
|
|
885
|
+
return sqlTransformedWhere();
|
|
886
|
+
} }
|
|
887
|
+
};
|
|
888
|
+
};
|
|
889
|
+
/**
|
|
890
|
+
* Packs the filters into a response object that can be applied for queries by the user
|
|
891
|
+
*/
|
|
892
|
+
return function transformToResponse(queryFilters, columnMask) {
|
|
893
|
+
const ret = internalTransformer(queryFilters);
|
|
894
|
+
ret.merge = (filters) => internalTransformer(mergeFilters(ret.query.many, filters), filters.limit);
|
|
895
|
+
ret[columnMaskKey] = columnMask;
|
|
896
|
+
return ret;
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region lib/abilityBuilder/queryFilter.ts
|
|
657
901
|
function isDynamicQueryFilter(filter) {
|
|
658
902
|
return typeof filter === "function";
|
|
659
903
|
}
|
|
660
904
|
function isStaticQueryFilter(filter) {
|
|
661
905
|
return typeof filter !== "function";
|
|
662
906
|
}
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region lib/helpers/errorLogging.ts
|
|
909
|
+
function hasMessage(error) {
|
|
910
|
+
return typeof error === "object" && error !== null && "message" in error;
|
|
911
|
+
}
|
|
912
|
+
function toLoggedException(error) {
|
|
913
|
+
if (hasMessage(error)) {
|
|
914
|
+
const gqlError = error;
|
|
915
|
+
return {
|
|
916
|
+
"exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
|
|
917
|
+
"exception.message": String(gqlError.message),
|
|
918
|
+
...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
|
|
919
|
+
...gqlError.path ? { path: gqlError.path } : {},
|
|
920
|
+
...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
"exception.type": "UnknownError",
|
|
925
|
+
"exception.message": String(error)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Every erroneous rumble log carries the same `errors` array shape,
|
|
930
|
+
* whether it originates from a thrown exception (single-element array)
|
|
931
|
+
* or a GraphQL `ExecutionResult.errors` list (multi-element array).
|
|
932
|
+
*/
|
|
933
|
+
function errorsLogField(errors) {
|
|
934
|
+
return { errors: errors.map(toLoggedException) };
|
|
935
|
+
}
|
|
936
|
+
function errorLogField(error) {
|
|
937
|
+
return errorsLogField([error]);
|
|
938
|
+
}
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region lib/abilityBuilder/telemetry.ts
|
|
941
|
+
const makeNothingRegisteredWarner = (config, logger) => debounce((model, action) => {
|
|
942
|
+
const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
|
|
943
|
+
if (logger) logger.warn({
|
|
944
|
+
[ATTR_TABLE]: model,
|
|
945
|
+
[ATTR_ACTION]: action,
|
|
946
|
+
...traceCorrelationFields(config)
|
|
947
|
+
}, msg);
|
|
948
|
+
else console.warn(msg);
|
|
949
|
+
}, 1e3);
|
|
950
|
+
/**
|
|
951
|
+
* Runs the preparation of the abilities of one table and action, reporting
|
|
952
|
+
* the attributes it collects to the span and the logger.
|
|
953
|
+
*/
|
|
954
|
+
function traceAbilityPreparation({ otel, log, telemetryConfig }, tableName, action, assemble) {
|
|
955
|
+
const run = async (span) => {
|
|
956
|
+
const attributes = {
|
|
957
|
+
[ATTR_TABLE]: tableName,
|
|
958
|
+
[ATTR_ACTION]: action
|
|
959
|
+
};
|
|
960
|
+
try {
|
|
961
|
+
const result = await assemble(attributes);
|
|
962
|
+
log?.debug({
|
|
963
|
+
...attributes,
|
|
964
|
+
...traceCorrelationFields(telemetryConfig, span)
|
|
965
|
+
}, "abilities prepared");
|
|
966
|
+
return result;
|
|
967
|
+
} catch (error) {
|
|
968
|
+
recordSpanError(span, error);
|
|
969
|
+
log?.error({
|
|
970
|
+
...attributes,
|
|
971
|
+
...traceCorrelationFields(telemetryConfig, span),
|
|
972
|
+
...errorLogField(error)
|
|
973
|
+
}, "abilities failed");
|
|
974
|
+
throw error;
|
|
975
|
+
} finally {
|
|
976
|
+
span?.setAttributes(attributes);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, async (span) => {
|
|
980
|
+
try {
|
|
981
|
+
return await run(span);
|
|
982
|
+
} finally {
|
|
983
|
+
span.end();
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
return run();
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region lib/abilityBuilder/tableAbilities.ts
|
|
663
990
|
/**
|
|
664
991
|
* filter() is async. Without type checking (plain JS, `any`) a forgotten await would
|
|
665
992
|
* hand the promise itself to drizzle
|
|
@@ -677,114 +1004,223 @@ function guardAgainstMissingAwait(promise) {
|
|
|
677
1004
|
});
|
|
678
1005
|
return promise;
|
|
679
1006
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
},
|
|
1007
|
+
/**
|
|
1008
|
+
* A filter no row can match, since it requires the primary key to equal two
|
|
1009
|
+
* distinct values at once.
|
|
1010
|
+
*/
|
|
1011
|
+
function createBlockEverythingFilter(tableName, tableSchema) {
|
|
1012
|
+
if (Object.keys(tableSchema.primaryKey).length === 0) throw new RumbleError(`No primary key found for entity ${tableName}`);
|
|
1013
|
+
const [primaryKeyName, primaryKeyField] = Object.entries(tableSchema.primaryKey)[0];
|
|
1014
|
+
const distinctValues = createDistinctValuesFromSQLType(primaryKeyField.getSQLType());
|
|
1015
|
+
return { where: { AND: [{ [primaryKeyName]: distinctValues.value1 }, { [primaryKeyName]: distinctValues.value2 }] } };
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Creates the abilities of a single table, which resolve the registered
|
|
1019
|
+
* query filters for a request.
|
|
1020
|
+
*/
|
|
1021
|
+
const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
1022
|
+
const { db, actions } = settings;
|
|
1023
|
+
const tableSchema = tableHelper({
|
|
1024
|
+
db,
|
|
1025
|
+
table: tableName
|
|
1026
|
+
});
|
|
1027
|
+
const withRestrictingWhere = (filter) => {
|
|
1028
|
+
const where = realWhere(filter.where);
|
|
1029
|
+
return !!where && Object.keys(where).length > 0 && relationsFilterToSQL(tableSchema.table, sanitizeFilterValue(where), tableSchema.relations, db._.relations) !== void 0 || where === void 0 ? filter : {
|
|
1030
|
+
...filter,
|
|
1031
|
+
where: void 0
|
|
1032
|
+
};
|
|
1033
|
+
};
|
|
1034
|
+
const simpleQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
1035
|
+
const filters = queryFilters.get(action);
|
|
1036
|
+
if (!filters || filters === "unrestricted") return [action, []];
|
|
1037
|
+
return [action, filters.filter(isStaticQueryFilter).map(withRestrictingWhere)];
|
|
1038
|
+
}));
|
|
1039
|
+
const dynamicQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
1040
|
+
const filters = queryFilters.get(action);
|
|
1041
|
+
if (!filters || filters === "unrestricted") return [action, []];
|
|
1042
|
+
return [action, filters.filter(isDynamicQueryFilter)];
|
|
1043
|
+
}));
|
|
1044
|
+
const blockEverythingFilter = createBlockEverythingFilter(String(tableName), tableSchema);
|
|
1045
|
+
const resolveColumnAccess = createColumnAccessResolver(settings, tableSchema);
|
|
1046
|
+
const transformToResponse = createFilterResponder(settings, tableSchema);
|
|
1047
|
+
const assembleAbilities = async (userContext, action, attributes) => {
|
|
1048
|
+
const filters = queryFilters.get(action);
|
|
1049
|
+
if (filters === "unrestricted") {
|
|
1050
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
1051
|
+
return transformToResponse();
|
|
1052
|
+
}
|
|
1053
|
+
if (!filters) {
|
|
1054
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
1055
|
+
settings.warnNothingRegistered(String(tableName), action);
|
|
1056
|
+
return transformToResponse(blockEverythingFilter);
|
|
1057
|
+
}
|
|
1058
|
+
const rawResults = await Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
|
|
1059
|
+
if (rawResults.includes("allow")) {
|
|
1060
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
1061
|
+
return transformToResponse();
|
|
1062
|
+
}
|
|
1063
|
+
const dynamicResults = rawResults.filter((r) => r !== void 0).map(withRestrictingWhere);
|
|
1064
|
+
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
1065
|
+
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
1066
|
+
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
1067
|
+
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
1068
|
+
if (allQueryFilters.length === 0) {
|
|
1069
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
1070
|
+
return transformToResponse(blockEverythingFilter);
|
|
1071
|
+
}
|
|
1072
|
+
const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
|
|
1073
|
+
return mergeFilters(a, b, "OR");
|
|
1074
|
+
});
|
|
1075
|
+
const { columns, extras, mask } = resolveColumnAccess(action, allQueryFilters);
|
|
1076
|
+
if (columns) mergedFilters.columns = columns;
|
|
1077
|
+
else delete mergedFilters.columns;
|
|
1078
|
+
if (extras) mergedFilters.extras = {
|
|
1079
|
+
...mergedFilters.extras,
|
|
1080
|
+
...extras
|
|
1081
|
+
};
|
|
1082
|
+
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
1083
|
+
return transformToResponse(mergedFilters, mask);
|
|
1084
|
+
};
|
|
1085
|
+
return { withContext: (userContext) => {
|
|
1086
|
+
const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(userContext, action, attributes));
|
|
1087
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1088
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1089
|
+
const abilities = { filter: (action) => {
|
|
1090
|
+
let prepared = cache.get(action);
|
|
1091
|
+
if (!prepared) {
|
|
1092
|
+
prepared = guardAgainstMissingAwait(prepare(action));
|
|
1093
|
+
cache.set(action, prepared);
|
|
1094
|
+
prepared.then((result) => resolved.set(action, result), () => {});
|
|
1095
|
+
}
|
|
1096
|
+
return prepared;
|
|
1097
|
+
} };
|
|
1098
|
+
abilities[resolvedFilterKey] = (action) => resolved.get(action);
|
|
1099
|
+
return abilities;
|
|
1100
|
+
} };
|
|
1101
|
+
};
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region lib/abilityBuilder/tableBuilder.ts
|
|
1104
|
+
/**
|
|
1105
|
+
* Creates the registration API (allow/filter) of a single table.
|
|
1106
|
+
*/
|
|
1107
|
+
const createTableBuilder = ({ actions, isBuilt }) => {
|
|
1108
|
+
const queryFilters = /* @__PURE__ */ new Map();
|
|
1109
|
+
const runtimeFilters = /* @__PURE__ */ new Map();
|
|
1110
|
+
for (const action of actions) if (!runtimeFilters.has(action)) runtimeFilters.set(action, []);
|
|
1111
|
+
const registerFilter = (actions, prefetch) => ({
|
|
1112
|
+
/**
|
|
1113
|
+
* The actual filter function to apply. Returns the allowed values
|
|
1114
|
+
*/
|
|
1115
|
+
by: (explicitFilter) => {
|
|
1116
|
+
for (const action of actions) runtimeFilters.get(action).push(prefetch ? {
|
|
1117
|
+
filter: explicitFilter,
|
|
1118
|
+
prefetch
|
|
1119
|
+
} : { filter: explicitFilter });
|
|
1120
|
+
} });
|
|
1121
|
+
return {
|
|
1122
|
+
/**
|
|
1123
|
+
* Allows to perform a specific action on a specific entity
|
|
1124
|
+
*/
|
|
1125
|
+
allow: (action) => {
|
|
1126
|
+
if (isBuilt()) throw new RumbleError("You can't call allow() after the ability builder has been built. Please ensure that you register all abilities before accessing them.");
|
|
1127
|
+
const actions = Array.isArray(action) ? action : [action];
|
|
1128
|
+
const setUnrestrictedByThisCall = /* @__PURE__ */ new Set();
|
|
1129
|
+
for (const action of actions) {
|
|
1130
|
+
let filters = queryFilters.get(action);
|
|
1131
|
+
if (!filters) {
|
|
1132
|
+
filters = "unrestricted";
|
|
1133
|
+
queryFilters.set(action, filters);
|
|
1134
|
+
setUnrestrictedByThisCall.add(action);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
/**
|
|
1139
|
+
* Restricts the allowed actions to a filter
|
|
1140
|
+
* @example
|
|
1141
|
+
* ```ts
|
|
1142
|
+
* abilityBuilder.users.allow(["read", "update", "delete"]).when(({ userId }) => ({
|
|
1143
|
+
* where: {
|
|
1144
|
+
* id: userId,
|
|
1145
|
+
* },
|
|
1146
|
+
* }));
|
|
1147
|
+
* ```
|
|
1148
|
+
*/
|
|
1149
|
+
when: (queryFilter) => {
|
|
1150
|
+
for (const action of actions) {
|
|
1151
|
+
if (queryFilters.get(action) === "unrestricted") {
|
|
1152
|
+
if (setUnrestrictedByThisCall.has(action)) queryFilters.set(action, []);
|
|
1153
|
+
else continue;
|
|
1154
|
+
}
|
|
1155
|
+
queryFilters.get(action).push(queryFilter);
|
|
1156
|
+
}
|
|
1157
|
+
} };
|
|
1158
|
+
},
|
|
1159
|
+
/**
|
|
1160
|
+
* Allows to register an application level filter to restrict some results
|
|
1161
|
+
* which were returned by a query
|
|
1162
|
+
*/
|
|
1163
|
+
filter: (action) => {
|
|
1164
|
+
const actions = Array.isArray(action) ? action : [action];
|
|
1165
|
+
return {
|
|
1166
|
+
/**
|
|
1167
|
+
* Allows to register an application level prefetch to fetch some data
|
|
1168
|
+
* which could be useful for later filtering the results. The prefetch
|
|
1169
|
+
* function will be called with the user context but unlike the actual
|
|
1170
|
+
* filter function, it will not have access to the result of the query
|
|
1171
|
+
* and therefore can be run in parallel with underlying query resolver.
|
|
1172
|
+
* A typical use case is to fetch some data which is not directly
|
|
1173
|
+
* related to the query but to the context only. So e.g. fetching the
|
|
1174
|
+
* user's permissions from an external system and then later applying
|
|
1175
|
+
* the filter based on those permissions.
|
|
1176
|
+
*/
|
|
1177
|
+
prefetch: (prefetch) => registerFilter(actions, prefetch),
|
|
1178
|
+
...registerFilter(actions)
|
|
1179
|
+
};
|
|
1180
|
+
},
|
|
1181
|
+
_: {
|
|
1182
|
+
runtimeFilters,
|
|
1183
|
+
queryFilters
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
};
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region lib/abilityBuilder/index.ts
|
|
689
1189
|
const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
|
|
690
1190
|
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
691
1191
|
const telemetryConfig = {
|
|
692
1192
|
otel,
|
|
693
1193
|
logger: loggerConfig
|
|
694
1194
|
};
|
|
695
|
-
const
|
|
1195
|
+
const settings = {
|
|
1196
|
+
db,
|
|
1197
|
+
actions,
|
|
1198
|
+
defaultLimit,
|
|
1199
|
+
otel,
|
|
1200
|
+
telemetryConfig,
|
|
1201
|
+
log,
|
|
1202
|
+
warnNothingRegistered: makeNothingRegisteredWarner(telemetryConfig, log)
|
|
1203
|
+
};
|
|
696
1204
|
const tableRelationNames = Object.keys(db.query).filter((name) => tableHelper({
|
|
697
1205
|
db,
|
|
698
1206
|
table: name
|
|
699
1207
|
}).isTable);
|
|
700
1208
|
let hasBeenBuilt = false;
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
filters = "unrestricted";
|
|
717
|
-
queryFilters.set(action, filters);
|
|
718
|
-
setUnrestrictedByThisCall.add(action);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
/**
|
|
723
|
-
* Restricts the allowed actions to a filter
|
|
724
|
-
* @example
|
|
725
|
-
* ```ts
|
|
726
|
-
* abilityBuilder.users.allow(["read", "update", "delete"]).when(({ userId }) => ({
|
|
727
|
-
* where: {
|
|
728
|
-
* id: userId,
|
|
729
|
-
* },
|
|
730
|
-
* }));
|
|
731
|
-
* ```
|
|
732
|
-
*/
|
|
733
|
-
when: (queryFilter) => {
|
|
734
|
-
for (const action of actions) {
|
|
735
|
-
if (queryFilters.get(action) === "unrestricted") {
|
|
736
|
-
if (setUnrestrictedByThisCall.has(action)) queryFilters.set(action, []);
|
|
737
|
-
else continue;
|
|
738
|
-
}
|
|
739
|
-
queryFilters.get(action).push(queryFilter);
|
|
740
|
-
}
|
|
741
|
-
} };
|
|
742
|
-
},
|
|
743
|
-
/**
|
|
744
|
-
* Allows to register an application level filter to restrict some results
|
|
745
|
-
* which were returned by a query
|
|
746
|
-
*/
|
|
747
|
-
filter: (action) => {
|
|
748
|
-
const actions = Array.isArray(action) ? action : [action];
|
|
749
|
-
return {
|
|
750
|
-
/**
|
|
751
|
-
* Allows to register an application level prefetch to fetch some data
|
|
752
|
-
* which could be useful for later filtering the results. The prefetch
|
|
753
|
-
* function will be called with the user context but unlike the actual
|
|
754
|
-
* filter function, it will not have access to the result of the query
|
|
755
|
-
* and therefore can be run in parallel with underlying query resolver.
|
|
756
|
-
* A typical use case is to fetch some data which is not directly
|
|
757
|
-
* related to the query but to the context only. So e.g. fetching the
|
|
758
|
-
* user's permissions from an external system and then later applying
|
|
759
|
-
* the filter based on those permissions.
|
|
760
|
-
*/
|
|
761
|
-
prefetch: (prefetch) => {
|
|
762
|
-
return {
|
|
763
|
-
/**
|
|
764
|
-
* The actual filter function to apply. Returns the allowed values
|
|
765
|
-
*/
|
|
766
|
-
by: (explicitFilter) => {
|
|
767
|
-
for (const action of actions) runtimeFilters.get(action).push({
|
|
768
|
-
filter: explicitFilter,
|
|
769
|
-
prefetch
|
|
770
|
-
});
|
|
771
|
-
} };
|
|
772
|
-
},
|
|
773
|
-
/**
|
|
774
|
-
* The actual filter function to apply. Returns the allowed values
|
|
775
|
-
*/
|
|
776
|
-
by: (explicitFilter) => {
|
|
777
|
-
for (const action of actions) runtimeFilters.get(action).push({ filter: explicitFilter });
|
|
778
|
-
}
|
|
779
|
-
};
|
|
780
|
-
},
|
|
781
|
-
_: {
|
|
782
|
-
runtimeFilters,
|
|
783
|
-
queryFilters
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
};
|
|
787
|
-
const buildersPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createBuilderForTable()]));
|
|
1209
|
+
const buildersPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createTableBuilder({
|
|
1210
|
+
actions: settings.actions,
|
|
1211
|
+
isBuilt: () => hasBeenBuilt
|
|
1212
|
+
})]));
|
|
1213
|
+
const { maskColumns, maskedActions, primaryKeyHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
|
|
1214
|
+
const tableSchema = tableHelper({
|
|
1215
|
+
db,
|
|
1216
|
+
table: tableName
|
|
1217
|
+
});
|
|
1218
|
+
return [tableName, {
|
|
1219
|
+
columns: Object.keys(tableSchema.columns),
|
|
1220
|
+
primaryKey: Object.keys(tableSchema.primaryKey)
|
|
1221
|
+
}];
|
|
1222
|
+
})));
|
|
1223
|
+
const readActionPerTable = /* @__PURE__ */ new Map();
|
|
788
1224
|
return {
|
|
789
1225
|
...buildersPerTable,
|
|
790
1226
|
/**
|
|
@@ -792,247 +1228,28 @@ by: (explicitFilter) => {
|
|
|
792
1228
|
* @ignore
|
|
793
1229
|
*/
|
|
794
1230
|
_: {
|
|
1231
|
+
maskColumns: (input) => maskColumns(input),
|
|
1232
|
+
/** The actions rows of the table were masked with in this request. */
|
|
1233
|
+
maskedActions: (abilities, table) => maskedActions(abilities, table),
|
|
1234
|
+
/** Whether masking in this request hid the primary key of the row. */
|
|
1235
|
+
primaryKeyHidden: (abilities, row) => primaryKeyHidden(abilities, row),
|
|
1236
|
+
registerReadAction(table, action) {
|
|
1237
|
+
readActionPerTable.set(table, action);
|
|
1238
|
+
},
|
|
1239
|
+
readActionOf(table) {
|
|
1240
|
+
return readActionPerTable.get(table) ?? "read";
|
|
1241
|
+
},
|
|
795
1242
|
registeredFilters({ action, table }) {
|
|
796
1243
|
return buildersPerTable[table]._.runtimeFilters.get(action);
|
|
797
1244
|
},
|
|
798
1245
|
build() {
|
|
799
|
-
const
|
|
800
|
-
const queryFilters = buildersPerTable[tableName]._.queryFilters;
|
|
801
|
-
const simpleQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
802
|
-
const filters = queryFilters.get(action);
|
|
803
|
-
if (!filters || filters === "unrestricted") return [action, []];
|
|
804
|
-
return [action, filters.filter(isStaticQueryFilter)];
|
|
805
|
-
}));
|
|
806
|
-
const dynamicQueryFilters = Object.fromEntries(actions.map((action) => {
|
|
807
|
-
const filters = queryFilters.get(action);
|
|
808
|
-
if (!filters || filters === "unrestricted") return [action, []];
|
|
809
|
-
return [action, filters.filter(isDynamicQueryFilter)];
|
|
810
|
-
}));
|
|
811
|
-
const tableSchema = tableHelper({
|
|
812
|
-
db,
|
|
813
|
-
table: tableName
|
|
814
|
-
});
|
|
815
|
-
if (Object.keys(tableSchema.primaryKey).length === 0) throw new RumbleError(`No primary key found for entity ${String(tableName)}`);
|
|
816
|
-
const primaryKeyField = Object.values(tableSchema.primaryKey)[0];
|
|
817
|
-
const distinctValues = createDistinctValuesFromSQLType(primaryKeyField.getSQLType());
|
|
818
|
-
const blockEverythingFilter = { where: { AND: [{ [primaryKeyField.name]: distinctValues.value1 }, { [primaryKeyField.name]: distinctValues.value2 }] } };
|
|
819
|
-
/**
|
|
820
|
-
* Packs the filters into a response object that can be applied for queries by the user
|
|
821
|
-
*/
|
|
822
|
-
function transformToResponse(queryFilters) {
|
|
823
|
-
const internalTransformer = (filters, mergedLimit) => {
|
|
824
|
-
const limit = lazy(() => {
|
|
825
|
-
if (mergedLimit !== void 0) {
|
|
826
|
-
if (!filters?.limit) return mergedLimit;
|
|
827
|
-
if (filters.limit > mergedLimit) return mergedLimit;
|
|
828
|
-
}
|
|
829
|
-
let limit = filters?.limit;
|
|
830
|
-
if (defaultLimit && (limit === void 0 || limit > defaultLimit)) limit = defaultLimit;
|
|
831
|
-
return limit ?? void 0;
|
|
832
|
-
});
|
|
833
|
-
const sqlTransformedWhere = lazy(() => {
|
|
834
|
-
const casing = db._.session?.dialect?.casing;
|
|
835
|
-
return filters?.where ? relationsFilterToSQL(tableSchema.foundRelation.table, sanitizeFilterValue(filters.where), tableSchema.relations, db._.relations, casing) : void 0;
|
|
836
|
-
});
|
|
837
|
-
if (filters?.columns) return {
|
|
838
|
-
/**
|
|
839
|
-
* Query filters for the drizzle query API.
|
|
840
|
-
* @example
|
|
841
|
-
* ```ts
|
|
842
|
-
* author: t.relation("author", {
|
|
843
|
-
* query: async (_args, ctx) => (await ctx.abilities.users.filter("read")).query.single,
|
|
844
|
-
* }),
|
|
845
|
-
* ´´´
|
|
846
|
-
*/
|
|
847
|
-
query: {
|
|
848
|
-
/**
|
|
849
|
-
* For find first calls
|
|
850
|
-
*/
|
|
851
|
-
single: {
|
|
852
|
-
extras: filters?.extras,
|
|
853
|
-
where: sanitizeFilterValue(filters?.where),
|
|
854
|
-
columns: filters?.columns
|
|
855
|
-
},
|
|
856
|
-
/**
|
|
857
|
-
* For find many calls
|
|
858
|
-
*/
|
|
859
|
-
many: {
|
|
860
|
-
extras: filters?.extras,
|
|
861
|
-
where: sanitizeFilterValue(filters?.where),
|
|
862
|
-
columns: filters?.columns,
|
|
863
|
-
get limit() {
|
|
864
|
-
return limit();
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
},
|
|
868
|
-
/**
|
|
869
|
-
* Query filters for the drizzle SQL API as used in e.g. updates.
|
|
870
|
-
* @example
|
|
871
|
-
*
|
|
872
|
-
* ```ts
|
|
873
|
-
* await db
|
|
874
|
-
* .update(schema.users)
|
|
875
|
-
* .set({
|
|
876
|
-
* name: args.newName,
|
|
877
|
-
* })
|
|
878
|
-
* .where(
|
|
879
|
-
* and(
|
|
880
|
-
* eq(schema.users.id, args.userId),
|
|
881
|
-
* (await ctx.abilities.users.filter("update")).sql.where,
|
|
882
|
-
* ),
|
|
883
|
-
* );
|
|
884
|
-
* ```
|
|
885
|
-
*
|
|
886
|
-
*/
|
|
887
|
-
sql: { get where() {
|
|
888
|
-
return sqlTransformedWhere();
|
|
889
|
-
} }
|
|
890
|
-
};
|
|
891
|
-
else return {
|
|
892
|
-
/**
|
|
893
|
-
* Query filters for the drizzle query API.
|
|
894
|
-
* @example
|
|
895
|
-
* ```ts
|
|
896
|
-
* author: t.relation("author", {
|
|
897
|
-
* query: async (_args, ctx) => (await ctx.abilities.users.filter("read")).query.single,
|
|
898
|
-
* }),
|
|
899
|
-
* ´´´
|
|
900
|
-
*/
|
|
901
|
-
query: {
|
|
902
|
-
/**
|
|
903
|
-
* For find first calls
|
|
904
|
-
*/
|
|
905
|
-
single: {
|
|
906
|
-
extras: filters?.extras,
|
|
907
|
-
where: sanitizeFilterValue(filters?.where)
|
|
908
|
-
},
|
|
909
|
-
/**
|
|
910
|
-
* For find many calls
|
|
911
|
-
*/
|
|
912
|
-
many: {
|
|
913
|
-
extras: filters?.extras,
|
|
914
|
-
where: sanitizeFilterValue(filters?.where),
|
|
915
|
-
get limit() {
|
|
916
|
-
return limit();
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
},
|
|
920
|
-
/**
|
|
921
|
-
* Query filters for the drizzle SQL API as used in e.g. updates.
|
|
922
|
-
* @example
|
|
923
|
-
*
|
|
924
|
-
* ```ts
|
|
925
|
-
* await db
|
|
926
|
-
* .update(schema.users)
|
|
927
|
-
* .set({
|
|
928
|
-
* name: args.newName,
|
|
929
|
-
* })
|
|
930
|
-
* .where(
|
|
931
|
-
* and(
|
|
932
|
-
* eq(schema.users.id, args.userId),
|
|
933
|
-
* (await ctx.abilities.users.filter("update")).sql.where,
|
|
934
|
-
* ),
|
|
935
|
-
* );
|
|
936
|
-
* ```
|
|
937
|
-
*
|
|
938
|
-
*/
|
|
939
|
-
sql: { get where() {
|
|
940
|
-
return sqlTransformedWhere();
|
|
941
|
-
} }
|
|
942
|
-
};
|
|
943
|
-
};
|
|
944
|
-
const ret = internalTransformer(queryFilters);
|
|
945
|
-
/**
|
|
946
|
-
* Merges the current query filters with the provided filters for this call only
|
|
947
|
-
*/
|
|
948
|
-
function merge(p) {
|
|
949
|
-
const merged = mergeFilters(ret.query.many, p);
|
|
950
|
-
return internalTransformer(merged, p.limit);
|
|
951
|
-
}
|
|
952
|
-
ret.merge = merge;
|
|
953
|
-
return ret;
|
|
954
|
-
}
|
|
955
|
-
return { withContext: (userContext) => {
|
|
956
|
-
const prepare = (action) => {
|
|
957
|
-
const assembleAbilities = async (attributes) => {
|
|
958
|
-
const filters = queryFilters.get(action);
|
|
959
|
-
if (filters === "unrestricted") {
|
|
960
|
-
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
961
|
-
return transformToResponse();
|
|
962
|
-
}
|
|
963
|
-
if (!filters) {
|
|
964
|
-
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
965
|
-
nothingRegisteredWarningLogger(String(tableName), action);
|
|
966
|
-
return transformToResponse(blockEverythingFilter);
|
|
967
|
-
}
|
|
968
|
-
const rawResults = await Promise.all(dynamicQueryFilters[action].map((func) => func(userContext)));
|
|
969
|
-
if (rawResults.includes("allow")) {
|
|
970
|
-
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
971
|
-
return transformToResponse();
|
|
972
|
-
}
|
|
973
|
-
const dynamicResults = rawResults.filter((r) => r !== void 0);
|
|
974
|
-
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
975
|
-
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
976
|
-
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
977
|
-
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
978
|
-
if (allQueryFilters.length === 0) {
|
|
979
|
-
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
980
|
-
return transformToResponse(blockEverythingFilter);
|
|
981
|
-
}
|
|
982
|
-
const mergedFilters = allQueryFilters.length === 1 ? allQueryFilters[0] : allQueryFilters.reduce((a, b) => {
|
|
983
|
-
return mergeFilters(a, b, "OR");
|
|
984
|
-
});
|
|
985
|
-
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
986
|
-
return transformToResponse(mergedFilters);
|
|
987
|
-
};
|
|
988
|
-
const run = async (span) => {
|
|
989
|
-
const attributes = {
|
|
990
|
-
[ATTR_TABLE]: String(tableName),
|
|
991
|
-
[ATTR_ACTION]: action
|
|
992
|
-
};
|
|
993
|
-
try {
|
|
994
|
-
const result = await assembleAbilities(attributes);
|
|
995
|
-
log?.debug({
|
|
996
|
-
...attributes,
|
|
997
|
-
...traceCorrelationFields(telemetryConfig, span)
|
|
998
|
-
}, "abilities prepared");
|
|
999
|
-
return result;
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
recordSpanError(span, error);
|
|
1002
|
-
log?.error({
|
|
1003
|
-
...attributes,
|
|
1004
|
-
...traceCorrelationFields(telemetryConfig, span),
|
|
1005
|
-
...errorLogField(error)
|
|
1006
|
-
}, "abilities failed");
|
|
1007
|
-
throw error;
|
|
1008
|
-
} finally {
|
|
1009
|
-
span?.setAttributes(attributes);
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1012
|
-
if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, async (span) => {
|
|
1013
|
-
try {
|
|
1014
|
-
return await run(span);
|
|
1015
|
-
} finally {
|
|
1016
|
-
span.end();
|
|
1017
|
-
}
|
|
1018
|
-
});
|
|
1019
|
-
return run();
|
|
1020
|
-
};
|
|
1021
|
-
const cache = /* @__PURE__ */ new Map();
|
|
1022
|
-
return { filter: (action) => {
|
|
1023
|
-
let prepared = cache.get(action);
|
|
1024
|
-
if (!prepared) {
|
|
1025
|
-
prepared = guardAgainstMissingAwait(prepare(action));
|
|
1026
|
-
cache.set(action, prepared);
|
|
1027
|
-
}
|
|
1028
|
-
return prepared;
|
|
1029
|
-
} };
|
|
1030
|
-
} };
|
|
1031
|
-
};
|
|
1032
|
-
const abilitiesPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createFilterForTable(tableName)]));
|
|
1246
|
+
const abilitiesPerTable = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, createTableAbilities(settings, tableName, buildersPerTable[tableName]._.queryFilters)]));
|
|
1033
1247
|
hasBeenBuilt = true;
|
|
1034
1248
|
return (ctx) => {
|
|
1035
|
-
|
|
1249
|
+
const state = createRequestState();
|
|
1250
|
+
const abilities = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
|
|
1251
|
+
attachRequestState(abilities, state);
|
|
1252
|
+
return abilities;
|
|
1036
1253
|
};
|
|
1037
1254
|
}
|
|
1038
1255
|
}
|
|
@@ -1856,7 +2073,8 @@ async function initSearchIfApplicable(input) {
|
|
|
1856
2073
|
*/
|
|
1857
2074
|
function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
|
|
1858
2075
|
if (search?.enabled && args.search && args.search.length > 0) {
|
|
1859
|
-
const
|
|
2076
|
+
const columnMask = abilities[columnMaskKey];
|
|
2077
|
+
const columnsToSearch = (columnMask ? Object.entries(tableSchema.columns).filter(([key]) => columnMask.guaranteed.has(key)) : Object.entries(tableSchema.columns)).filter(([_key, col]) => isStringLikeSQLTypeString(col.getSQLType()) || isIDLikeSQLTypeString(col.getSQLType()));
|
|
1860
2078
|
const searchParam = sql`${args.search}`;
|
|
1861
2079
|
args.extras = { search_distance: (table) => sql`${sql.join(columnsToSearch.map(([key]) => {
|
|
1862
2080
|
return sql`COALESCE((${table[key]}::TEXT <-> ${searchParam}), 1)`;
|
|
@@ -1914,14 +2132,15 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
1914
2132
|
db,
|
|
1915
2133
|
table
|
|
1916
2134
|
});
|
|
2135
|
+
abilityBuilder._.registerReadAction(table, readAction);
|
|
1917
2136
|
if (Object.keys(tableSchema.primaryKey).length === 0) warn(`Could not find primary key for ${String(table)}. Cannot register subscriptions!`, { [ATTR_TABLE]: String(table) });
|
|
1918
|
-
const
|
|
2137
|
+
const primaryKeyName = Object.keys(tableSchema.primaryKey)[0];
|
|
1919
2138
|
const { registerOnInstance } = makePubSubInstance({ table });
|
|
1920
2139
|
return schemaBuilder.drizzleObject(table, {
|
|
1921
2140
|
name: refName ?? capitalize(String(table)),
|
|
1922
2141
|
subscribe: (subscriptions, element, _context) => {
|
|
1923
|
-
if (!
|
|
1924
|
-
const primaryKeyValue = element[
|
|
2142
|
+
if (!primaryKeyName) return;
|
|
2143
|
+
const primaryKeyValue = element[primaryKeyName];
|
|
1925
2144
|
if (!primaryKeyValue) {
|
|
1926
2145
|
warn(`Could not find primary key value for element on ${String(table)}. Cannot register subscription!`, { [ATTR_TABLE]: String(table) });
|
|
1927
2146
|
return;
|
|
@@ -1936,6 +2155,16 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
1936
2155
|
table,
|
|
1937
2156
|
action: readAction
|
|
1938
2157
|
}),
|
|
2158
|
+
maskColumns: ({ context, entities, action }) => abilityBuilder._.maskColumns({
|
|
2159
|
+
table,
|
|
2160
|
+
action: action ?? readAction,
|
|
2161
|
+
abilities: context.abilities,
|
|
2162
|
+
entities
|
|
2163
|
+
}),
|
|
2164
|
+
maskPrimaryKeys: {
|
|
2165
|
+
fields: Object.keys(tableSchema.primaryKey),
|
|
2166
|
+
isHidden: (context, row) => abilityBuilder._.primaryKeyHidden(context.abilities, row)
|
|
2167
|
+
},
|
|
1939
2168
|
fields: (t) => {
|
|
1940
2169
|
const columns = tableSchema.columns;
|
|
1941
2170
|
const configMap = /* @__PURE__ */ new Map();
|
|
@@ -2171,7 +2400,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2171
2400
|
db,
|
|
2172
2401
|
table
|
|
2173
2402
|
});
|
|
2174
|
-
const
|
|
2403
|
+
const primaryKeyName = Object.keys(tableSchema.primaryKey)[0];
|
|
2175
2404
|
const { registerOnInstance } = makePubSubInstance({ table });
|
|
2176
2405
|
const pluralName = pluralize.plural(String(table));
|
|
2177
2406
|
const singularName = pluralize.singular(String(table));
|
|
@@ -2199,6 +2428,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2199
2428
|
type: [table],
|
|
2200
2429
|
nullable: false,
|
|
2201
2430
|
smartSubscription: true,
|
|
2431
|
+
maskColumnsAction: listAction,
|
|
2202
2432
|
description: `List all ${pluralName}`,
|
|
2203
2433
|
subscribe: (subscriptions, _root, _args, _ctx, _info) => {
|
|
2204
2434
|
registerOnInstance({
|
|
@@ -2246,11 +2476,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2246
2476
|
type: table,
|
|
2247
2477
|
nullable: false,
|
|
2248
2478
|
smartSubscription: true,
|
|
2479
|
+
maskColumnsAction: readAction,
|
|
2249
2480
|
description: `Get a single ${singularName} by ID`,
|
|
2250
2481
|
args: { id: t.arg.id({ required: true }) },
|
|
2251
2482
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
2252
2483
|
deepSetProto(args);
|
|
2253
|
-
const filter = (await ctx.abilities[table].filter(readAction)).merge({ where: { [
|
|
2484
|
+
const filter = (await ctx.abilities[table].filter(readAction)).merge({ where: { [primaryKeyName]: args.id } }).query.single;
|
|
2254
2485
|
const queryArgs = query(filter);
|
|
2255
2486
|
if (filter.columns) queryArgs.columns = filter.columns;
|
|
2256
2487
|
return findFirst ? findFirst({
|
|
@@ -2518,6 +2749,108 @@ function implementDefaultWhereInputArgs(schemaBuilder) {
|
|
|
2518
2749
|
}) });
|
|
2519
2750
|
}
|
|
2520
2751
|
//#endregion
|
|
2752
|
+
//#region lib/helpers/objectTypeOptions.ts
|
|
2753
|
+
function objectTypeOptionsOfField(buildCache, fieldConfig) {
|
|
2754
|
+
const type = fieldConfig.type;
|
|
2755
|
+
const ref = type.kind === "List" ? type.type.kind === "Object" ? type.type.ref : void 0 : type.kind === "Object" ? type.ref : void 0;
|
|
2756
|
+
if (!ref) return void 0;
|
|
2757
|
+
return buildCache.getTypeConfig(ref, "Object").pothosOptions;
|
|
2758
|
+
}
|
|
2759
|
+
//#endregion
|
|
2760
|
+
//#region lib/helpers/registerPlugin.ts
|
|
2761
|
+
const registered = /* @__PURE__ */ new Set();
|
|
2762
|
+
function registerPluginOnce(name, plugin) {
|
|
2763
|
+
if (registered.has(name)) return;
|
|
2764
|
+
SchemaBuilder.allowPluginReRegistration = true;
|
|
2765
|
+
SchemaBuilder.registerPlugin(name, plugin);
|
|
2766
|
+
registered.add(name);
|
|
2767
|
+
}
|
|
2768
|
+
//#endregion
|
|
2769
|
+
//#region lib/columnMaskPlugin/maskTypes.ts
|
|
2770
|
+
const maskColumnsKey = "maskColumns";
|
|
2771
|
+
const maskColumnsActionKey = "maskColumnsAction";
|
|
2772
|
+
const maskPrimaryKeysKey = "maskPrimaryKeys";
|
|
2773
|
+
const columnMaskPluginName = "ColumnMaskPlugin";
|
|
2774
|
+
//#endregion
|
|
2775
|
+
//#region lib/columnMaskPlugin/columnMaskPlugin.ts
|
|
2776
|
+
var ColumnMaskPlugin = class extends BasePlugin {
|
|
2777
|
+
wrapResolve(resolver, fieldConfig) {
|
|
2778
|
+
return this.maskResult(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig);
|
|
2779
|
+
}
|
|
2780
|
+
maskPrimaryKey(resolver, fieldConfig) {
|
|
2781
|
+
const primaryKeys = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskPrimaryKeysKey];
|
|
2782
|
+
if (!primaryKeys?.fields.includes(fieldConfig.name)) return resolver;
|
|
2783
|
+
return (parent, args, context, info) => primaryKeys.isHidden(context, parent) ? null : resolver(parent, args, context, info);
|
|
2784
|
+
}
|
|
2785
|
+
maskResult(resolver, fieldConfig) {
|
|
2786
|
+
const maskColumns = objectTypeOptionsOfField(this.buildCache, fieldConfig)?.[maskColumnsKey];
|
|
2787
|
+
if (!maskColumns) return resolver;
|
|
2788
|
+
const action = fieldConfig.pothosOptions[maskColumnsActionKey];
|
|
2789
|
+
const mask = (value, context) => {
|
|
2790
|
+
if (value == null) return value;
|
|
2791
|
+
if (Array.isArray(value) && value.some(isThenable)) return Promise.all(value).then((rows) => mask(rows, context));
|
|
2792
|
+
const pending = maskColumns({
|
|
2793
|
+
context,
|
|
2794
|
+
entities: Array.isArray(value) ? value : [value],
|
|
2795
|
+
action
|
|
2796
|
+
});
|
|
2797
|
+
return pending ? pending.then(() => value) : value;
|
|
2798
|
+
};
|
|
2799
|
+
return (parent, args, context, info) => {
|
|
2800
|
+
const resolved = resolver(parent, args, context, info);
|
|
2801
|
+
return isThenable(resolved) ? resolved.then((value) => mask(value, context)) : mask(resolved, context);
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2805
|
+
function registerColumnMaskPlugin() {
|
|
2806
|
+
registerPluginOnce(columnMaskPluginName, ColumnMaskPlugin);
|
|
2807
|
+
}
|
|
2808
|
+
//#endregion
|
|
2809
|
+
//#region lib/helpers/abilityAwareClient.ts
|
|
2810
|
+
function createAbilityAwareClient({ db, abilityBuilder }) {
|
|
2811
|
+
return (context) => {
|
|
2812
|
+
const apis = /* @__PURE__ */ new Map();
|
|
2813
|
+
const query = new Proxy(db.query, { get(target, table, receiver) {
|
|
2814
|
+
const api = Reflect.get(target, table, receiver);
|
|
2815
|
+
if (typeof table !== "string" || !context.abilities?.[table]) return api;
|
|
2816
|
+
let wrapped = apis.get(table);
|
|
2817
|
+
if (!wrapped) {
|
|
2818
|
+
wrapped = Object.create(api, { findMany: { value: async (config = {}) => {
|
|
2819
|
+
const masked = abilityBuilder._.maskedActions(context.abilities, table);
|
|
2820
|
+
const actions = masked.size ? [...masked] : [abilityBuilder._.readActionOf(table)];
|
|
2821
|
+
return (await Promise.all(actions.map(async (action) => {
|
|
2822
|
+
const ability = (await context.abilities[table].filter(action)).query.many;
|
|
2823
|
+
const { where, extras } = mergeFilters({
|
|
2824
|
+
where: config.where,
|
|
2825
|
+
extras: config.extras
|
|
2826
|
+
}, {
|
|
2827
|
+
where: ability.where,
|
|
2828
|
+
extras: ability.extras
|
|
2829
|
+
});
|
|
2830
|
+
const rows = await api.findMany({
|
|
2831
|
+
...config,
|
|
2832
|
+
where,
|
|
2833
|
+
extras
|
|
2834
|
+
});
|
|
2835
|
+
await abilityBuilder._.maskColumns({
|
|
2836
|
+
table,
|
|
2837
|
+
action,
|
|
2838
|
+
abilities: context.abilities,
|
|
2839
|
+
entities: rows
|
|
2840
|
+
});
|
|
2841
|
+
return rows;
|
|
2842
|
+
}))).flat();
|
|
2843
|
+
} } });
|
|
2844
|
+
apis.set(table, wrapped);
|
|
2845
|
+
}
|
|
2846
|
+
return wrapped;
|
|
2847
|
+
} });
|
|
2848
|
+
return new Proxy(db, { get(target, prop, receiver) {
|
|
2849
|
+
return prop === "query" ? query : Reflect.get(target, prop, receiver);
|
|
2850
|
+
} });
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
//#endregion
|
|
2521
2854
|
//#region lib/runtimeFiltersPlugin/filterTypes.ts
|
|
2522
2855
|
const pluginName = "RuntimeFiltersPlugin";
|
|
2523
2856
|
//#endregion
|
|
@@ -2528,14 +2861,9 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2528
2861
|
tracerEnabled;
|
|
2529
2862
|
logger;
|
|
2530
2863
|
telemetryConfig = {};
|
|
2531
|
-
filterLoaders = /* @__PURE__ */ new WeakMap();
|
|
2532
2864
|
getLoader(context, filter) {
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
perContext = /* @__PURE__ */ new Map();
|
|
2536
|
-
this.filterLoaders.set(context, perContext);
|
|
2537
|
-
}
|
|
2538
|
-
let loader = perContext.get(filter);
|
|
2865
|
+
const loaders = requestStateOf(context.abilities)?.filterLoaders ?? /* @__PURE__ */ new Map();
|
|
2866
|
+
let loader = loaders.get(filter);
|
|
2539
2867
|
if (!loader) {
|
|
2540
2868
|
const prefetchPromise = filter.prefetch ? filter.prefetch({ context }) : void 0;
|
|
2541
2869
|
loader = new DataLoader(async (entities) => {
|
|
@@ -2548,7 +2876,7 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2548
2876
|
const allowedSet = new Set(allowed);
|
|
2549
2877
|
return entities.map((entity) => allowedSet.has(entity) ? entity : null);
|
|
2550
2878
|
}, { cache: false });
|
|
2551
|
-
|
|
2879
|
+
loaders.set(filter, loader);
|
|
2552
2880
|
}
|
|
2553
2881
|
return loader;
|
|
2554
2882
|
}
|
|
@@ -2563,14 +2891,11 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2563
2891
|
return typeConfig;
|
|
2564
2892
|
}
|
|
2565
2893
|
wrapResolve(resolver, fieldConfig) {
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
else if (fieldType.kind === "Object") filters = fieldType.ref.currentConfig.pothosOptions[applyFiltersKey];
|
|
2571
|
-
if (!filters || !Array.isArray(filters) || filters.length === 0) return resolver(parent, args, context, info);
|
|
2894
|
+
const filters = objectTypeOptionsOfField(this.buildCache, fieldConfig)?.[applyFiltersKey];
|
|
2895
|
+
if (!filters) return resolver;
|
|
2896
|
+
const allFilters = Array.isArray(filters) ? filters : [filters];
|
|
2897
|
+
const applyFilters = async (parent, args, context, info) => {
|
|
2572
2898
|
const runFilters = async (span) => {
|
|
2573
|
-
const allFilters = Array.isArray(filters) ? filters : [filters];
|
|
2574
2899
|
span?.setAttribute(ATTR_FILTERS_TOTAL, allFilters.length);
|
|
2575
2900
|
const loaders = allFilters.map((filter) => this.getLoader(context, filter));
|
|
2576
2901
|
let resolved;
|
|
@@ -2625,15 +2950,11 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2625
2950
|
throw error;
|
|
2626
2951
|
}
|
|
2627
2952
|
};
|
|
2953
|
+
return (parent, args, context, info) => allFilters.length === 0 ? resolver(parent, args, context, info) : applyFilters(parent, args, context, info);
|
|
2628
2954
|
}
|
|
2629
2955
|
};
|
|
2630
|
-
let registered = false;
|
|
2631
2956
|
function registerRuntimeFiltersPlugin() {
|
|
2632
|
-
|
|
2633
|
-
SchemaBuilder.allowPluginReRegistration = true;
|
|
2634
|
-
SchemaBuilder.registerPlugin(pluginName, RuntimeFiltersPlugin);
|
|
2635
|
-
registered = true;
|
|
2636
|
-
}
|
|
2957
|
+
registerPluginOnce(pluginName, RuntimeFiltersPlugin);
|
|
2637
2958
|
}
|
|
2638
2959
|
//#endregion
|
|
2639
2960
|
//#region lib/scalars/addressValidation.ts
|
|
@@ -2846,12 +3167,14 @@ const PhoneNumberResolver = new GraphQLScalarType({
|
|
|
2846
3167
|
});
|
|
2847
3168
|
//#endregion
|
|
2848
3169
|
//#region lib/schemaBuilder.ts
|
|
2849
|
-
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation }) => {
|
|
3170
|
+
const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation, abilityBuilder }) => {
|
|
2850
3171
|
const createSpan = otel?.enabled && otel.tracer ? createOpenTelemetryWrapper(otel.tracer, otel.options) : void 0;
|
|
2851
3172
|
registerRuntimeFiltersPlugin();
|
|
3173
|
+
registerColumnMaskPlugin();
|
|
2852
3174
|
const schemaBuilder = new SchemaBuilder({
|
|
2853
3175
|
...pothosConfig,
|
|
2854
3176
|
plugins: [
|
|
3177
|
+
columnMaskPluginName,
|
|
2855
3178
|
pluginName,
|
|
2856
3179
|
DrizzlePlugin,
|
|
2857
3180
|
SmartSubscriptionsPlugin,
|
|
@@ -2860,13 +3183,19 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validatio
|
|
|
2860
3183
|
...pothosConfig?.plugins ?? []
|
|
2861
3184
|
],
|
|
2862
3185
|
drizzle: {
|
|
2863
|
-
client:
|
|
3186
|
+
client: createAbilityAwareClient({
|
|
3187
|
+
db,
|
|
3188
|
+
abilityBuilder
|
|
3189
|
+
}),
|
|
2864
3190
|
relations: db._.relations,
|
|
2865
3191
|
getTableConfig(table) {
|
|
2866
|
-
|
|
3192
|
+
if (is(table, PgTable)) return getTableConfig(table);
|
|
3193
|
+
if (is(table, MySqlTable)) return getTableConfig$1(table);
|
|
3194
|
+
if (is(table, SQLiteTable)) return getTableConfig$2(table);
|
|
2867
3195
|
return {
|
|
2868
|
-
columns,
|
|
2869
|
-
primaryKeys:
|
|
3196
|
+
columns: isTable(table) ? Object.values(getTableColumns(table)) : [],
|
|
3197
|
+
primaryKeys: [],
|
|
3198
|
+
uniqueConstraints: []
|
|
2870
3199
|
};
|
|
2871
3200
|
}
|
|
2872
3201
|
},
|
|
@@ -2985,7 +3314,8 @@ export const r = rumble({
|
|
|
2985
3314
|
const { makePubSubInstance, pubsub } = createPubSubInstance({ ...rumbleInput });
|
|
2986
3315
|
const { schemaBuilder } = createSchemaBuilder({
|
|
2987
3316
|
...rumbleInput,
|
|
2988
|
-
pubsub
|
|
3317
|
+
pubsub,
|
|
3318
|
+
abilityBuilder
|
|
2989
3319
|
});
|
|
2990
3320
|
const enum_ = createEnumImplementer({
|
|
2991
3321
|
...rumbleInput,
|