@m1212e/rumble 0.24.2 → 0.25.2

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/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.24.2";
189
+ var version = "0.25.2";
190
190
  //#endregion
191
- //#region lib/helpers/errorLogging.ts
192
- function hasMessage(error) {
193
- return typeof error === "object" && error !== null && "message" in error;
194
- }
195
- function toLoggedException(error) {
196
- if (hasMessage(error)) {
197
- const gqlError = error;
198
- return {
199
- "exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
200
- "exception.message": String(gqlError.message),
201
- ...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
202
- ...gqlError.path ? { path: gqlError.path } : {},
203
- ...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
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
- "exception.type": "UnknownError",
208
- "exception.message": String(error)
219
+ byTsName,
220
+ byDbName,
221
+ byTableRef
209
222
  };
210
223
  }
211
- /**
212
- * Every erroneous rumble log carries the same `errors` array shape,
213
- * whether it originates from a thrown exception (single-element array)
214
- * or a GraphQL `ExecutionResult.errors` list (multi-element array).
215
- */
216
- function errorsLogField(errors) {
217
- return { errors: errors.map(toLoggedException) };
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 errorLogField(error) {
220
- return errorsLogField([error]);
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: filterAWhere && filterBWhere ? mode === "OR" ? { OR: [filterAWhere, filterBWhere] } : { AND: [filterAWhere, filterBWhere] } : filterAWhere ?? filterBWhere,
240
- columns: filterA?.columns || filterB?.columns ? new Set([Object.entries(filterA?.columns ?? {}), Object.entries(filterB?.columns ?? {})].flat().filter(([, v]) => v === true).map(([k]) => k)).entries().reduce((acc, [key]) => {
241
- acc[key] = true;
242
- return acc;
243
- }, {}) : void 0,
244
- extras: filterA?.extras || filterB?.extras ? toMerged(filterA?.extras ?? {}, filterB?.extras ?? {}) : void 0,
245
- orderBy: filterA?.orderBy || filterB?.orderBy ? toMerged(filterA?.orderBy ?? {}, filterB?.orderBy ?? {}) : void 0,
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
- const makeNothingRegisteredWarner = (config, logger) => debounce((model, action) => {
681
- const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
682
- if (logger) logger.warn({
683
- [ATTR_TABLE]: model,
684
- [ATTR_ACTION]: action,
685
- ...traceCorrelationFields(config)
686
- }, msg);
687
- else console.warn(msg);
688
- }, 1e3);
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 nothingRegisteredWarningLogger = makeNothingRegisteredWarner(telemetryConfig, log);
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 createBuilderForTable = () => {
702
- const queryFilters = /* @__PURE__ */ new Map();
703
- const runtimeFilters = /* @__PURE__ */ new Map();
704
- for (const action of actions) if (!runtimeFilters.has(action)) runtimeFilters.set(action, []);
705
- return {
706
- /**
707
- * Allows to perform a specific action on a specific entity
708
- */
709
- allow: (action) => {
710
- if (hasBeenBuilt) 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.");
711
- const actions = Array.isArray(action) ? action : [action];
712
- const setUnrestrictedByThisCall = /* @__PURE__ */ new Set();
713
- for (const action of actions) {
714
- let filters = queryFilters.get(action);
715
- if (!filters) {
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 createFilterForTable = (tableName) => {
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
- return Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
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 columnsToSearch = (abilities.query.many.columns ? Object.entries(tableSchema.columns).filter(([key]) => abilities.query.many.columns[key]) : Object.entries(tableSchema.columns)).filter(([_key, col]) => isStringLikeSQLTypeString(col.getSQLType()) || isIDLikeSQLTypeString(col.getSQLType()));
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 primaryKey = Object.values(tableSchema.primaryKey)[0];
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 (!primaryKey) return;
1924
- const primaryKeyValue = element[primaryKey.name];
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 primaryKeyField = Object.values(tableSchema.primaryKey)[0];
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: { [primaryKeyField.name]: args.id } }).query.single;
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
- let perContext = this.filterLoaders.get(context);
2534
- if (!perContext) {
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
- perContext.set(filter, loader);
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
- return async (parent, args, context, info) => {
2567
- let filters;
2568
- const fieldType = fieldConfig?.type;
2569
- if (fieldType.kind === "List") filters = fieldType.type?.ref.currentConfig.pothosOptions[applyFiltersKey];
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
- if (!registered) {
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
@@ -2764,9 +3085,13 @@ function implementDefaultAddressTypes(schemaBuilder) {
2764
3085
  //#endregion
2765
3086
  //#region lib/scalars/emailAddress.ts
2766
3087
  /**
2767
- * Validates an email address using graphql-scalars' own format check, then lowercases it.
3088
+ * Validates an email address using graphql-scalars' own format check, then lowercases it, exactly
3089
+ * as the `EmailAddress` scalar does. Exported for values that do not arrive as GraphQL input,
3090
+ * such as OIDC claims.
3091
+ *
3092
+ * @throws RumbleErrorSafe if the value is not a valid email address
2768
3093
  */
2769
- const normalize$1 = (value) => {
3094
+ const normalizeEmailAddress = (value) => {
2770
3095
  try {
2771
3096
  return EmailAddressResolver.parseValue(value).toLowerCase();
2772
3097
  } catch (error) {
@@ -2777,23 +3102,28 @@ const EmailAddressResolver$1 = new GraphQLScalarType({
2777
3102
  name: "EmailAddress",
2778
3103
  description: `${EmailAddressResolver.description} Lowercased.`,
2779
3104
  specifiedByURL: EmailAddressResolver.specifiedByURL,
2780
- serialize: normalize$1,
2781
- parseValue: normalize$1,
3105
+ serialize: normalizeEmailAddress,
3106
+ parseValue: normalizeEmailAddress,
2782
3107
  parseLiteral(ast) {
2783
3108
  if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as email addresses but got a: ${ast.kind}`);
2784
- return normalize$1(ast.value);
3109
+ return normalizeEmailAddress(ast.value);
2785
3110
  }
2786
3111
  });
2787
3112
  //#endregion
2788
3113
  //#region lib/scalars/personName.ts
2789
3114
  const MAX_LENGTH = 200;
2790
3115
  const NAME_REGEX = /^[\p{L}\p{M}]+(?:(?:[ '-]|\.\x20?)[\p{L}\p{M}]+)*\.?$/u;
3116
+ const APOSTROPHE_VARIANTS = /[\u2018\u2019\u02BC\u00B4\u0060]/gu;
3117
+ const HYPHEN_VARIANTS = /[\u2010-\u2015\u2212]/gu;
2791
3118
  /**
2792
- * Normalizes and validates a person's name.
3119
+ * Normalizes and validates a person's name, exactly as the `PersonName` scalar does. Exported
3120
+ * for values that do not arrive as GraphQL input, such as OIDC claims.
3121
+ *
3122
+ * @throws RumbleErrorSafe if the value is not a valid name
2793
3123
  */
2794
- const normalize = (value) => {
3124
+ const normalizePersonName = (value) => {
2795
3125
  if (typeof value !== "string") throw new RumbleErrorSafe(`Name value is not a string: ${value}`);
2796
- const normalized = value.normalize("NFC").replace(/[\p{Cc}\p{Cf}]/gu, "").replace(/\p{Zs}+/gu, " ").trim().replace(/([-'])\1+/gu, "$1").replace(/^[-']+|[-']+$/gu, "").trim();
3126
+ const normalized = value.normalize("NFC").replace(/[\p{Cc}\p{Cf}]/gu, "").replace(APOSTROPHE_VARIANTS, "'").replace(HYPHEN_VARIANTS, "-").replace(/\p{Zs}+/gu, " ").trim().replace(/ ?- ?/gu, "-").replace(/([-'])\1+/gu, "$1").replace(/^[-']+|[-']+$/gu, "").trim();
2797
3127
  if (normalized.length === 0) throw new RumbleErrorSafe("Name must not be empty.");
2798
3128
  if (normalized.length > MAX_LENGTH) throw new RumbleErrorSafe(`Name must not be longer than ${MAX_LENGTH} characters.`);
2799
3129
  if (!NAME_REGEX.test(normalized)) throw new RumbleErrorSafe(`Invalid name: ${value}. Names may only contain letters and the separators space, apostrophe, hyphen and period, and may not start with or repeat a separator.`);
@@ -2801,12 +3131,12 @@ const normalize = (value) => {
2801
3131
  };
2802
3132
  const PersonNameResolver = new GraphQLScalarType({
2803
3133
  name: "PersonName",
2804
- description: "A person's name. Unicode letters from any script are allowed, along with space, apostrophe, hyphen and period as separators between name parts. The value is Unicode NFC normalized, has invisible/control characters stripped, has its whitespace collapsed, and has unambiguous typos (a doubled or stray leading/trailing hyphen/apostrophe) silently corrected rather than rejected.",
2805
- serialize: normalize,
2806
- parseValue: normalize,
3134
+ description: "A person's name. Unicode letters from any script are allowed, along with space, apostrophe, hyphen and period as separators between name parts. The value is Unicode NFC normalized, has invisible/control characters stripped, has its whitespace collapsed, and has unambiguous typos (typographic apostrophe or dash variants, spaces around a hyphen, a doubled or stray leading/trailing hyphen/apostrophe) silently corrected rather than rejected.",
3135
+ serialize: normalizePersonName,
3136
+ parseValue: normalizePersonName,
2807
3137
  parseLiteral(ast) {
2808
3138
  if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as names but got a: ${ast.kind}`);
2809
- return normalize(ast.value);
3139
+ return normalizePersonName(ast.value);
2810
3140
  }
2811
3141
  });
2812
3142
  //#endregion
@@ -2814,9 +3144,12 @@ const PersonNameResolver = new GraphQLScalarType({
2814
3144
  /**
2815
3145
  * Parses and validates a phone number using libphonenumber-js (Google's phone number metadata),
2816
3146
  * which checks real per-country validity (length, area code plausibility, ...) rather than just
2817
- * the E.164 shape.
3147
+ * the E.164 shape. This is what the `PhoneNumber` scalar does with its input; exported for values
3148
+ * that do not arrive as GraphQL input. `.number` of the result is the E.164 form.
3149
+ *
3150
+ * @throws RumbleErrorSafe if the value is not a valid phone number
2818
3151
  */
2819
- const parse = (value) => {
3152
+ const parsePhoneNumber = (value) => {
2820
3153
  if (typeof value !== "string") throw new RumbleErrorSafe(`Phone number value is not a string: ${value}`);
2821
3154
  const phoneNumber = parsePhoneNumberFromString(value);
2822
3155
  if (!phoneNumber?.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${value}. Please provide a valid phone number including its country calling code, e.g. +4915123456789.`);
@@ -2832,26 +3165,28 @@ const serialize = (value) => {
2832
3165
  if (!phoneNumber.isValid()) throw new RumbleErrorSafe(`Invalid phone number: ${phoneNumber.number}`);
2833
3166
  return phoneNumber.number;
2834
3167
  }
2835
- return parse(value).number;
3168
+ return parsePhoneNumber(value).number;
2836
3169
  };
2837
3170
  const PhoneNumberResolver = new GraphQLScalarType({
2838
3171
  name: "PhoneNumber",
2839
3172
  description: "A phone number, validated using real per-country validity rules and represented as a parsed libphonenumber-js PhoneNumber. On the wire it is transported as the E.164 format (https://en.wikipedia.org/wiki/E.164), e.g. +4915123456789. The country calling code is required.",
2840
3173
  serialize,
2841
- parseValue: parse,
3174
+ parseValue: parsePhoneNumber,
2842
3175
  parseLiteral(ast) {
2843
3176
  if (ast.kind !== Kind.STRING) throw new RumbleErrorSafe(`Can only validate strings as phone numbers but got a: ${ast.kind}`);
2844
- return parse(ast.value);
3177
+ return parsePhoneNumber(ast.value);
2845
3178
  }
2846
3179
  });
2847
3180
  //#endregion
2848
3181
  //#region lib/schemaBuilder.ts
2849
- const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation }) => {
3182
+ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validation, abilityBuilder }) => {
2850
3183
  const createSpan = otel?.enabled && otel.tracer ? createOpenTelemetryWrapper(otel.tracer, otel.options) : void 0;
2851
3184
  registerRuntimeFiltersPlugin();
3185
+ registerColumnMaskPlugin();
2852
3186
  const schemaBuilder = new SchemaBuilder({
2853
3187
  ...pothosConfig,
2854
3188
  plugins: [
3189
+ columnMaskPluginName,
2855
3190
  pluginName,
2856
3191
  DrizzlePlugin,
2857
3192
  SmartSubscriptionsPlugin,
@@ -2860,13 +3195,19 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger, validatio
2860
3195
  ...pothosConfig?.plugins ?? []
2861
3196
  ],
2862
3197
  drizzle: {
2863
- client: db,
3198
+ client: createAbilityAwareClient({
3199
+ db,
3200
+ abilityBuilder
3201
+ }),
2864
3202
  relations: db._.relations,
2865
3203
  getTableConfig(table) {
2866
- const columns = isTable(table) ? Object.values(getTableColumns(table)) : [];
3204
+ if (is(table, PgTable)) return getTableConfig(table);
3205
+ if (is(table, MySqlTable)) return getTableConfig$1(table);
3206
+ if (is(table, SQLiteTable)) return getTableConfig$2(table);
2867
3207
  return {
2868
- columns,
2869
- primaryKeys: columns.filter((v) => v.primary)
3208
+ columns: isTable(table) ? Object.values(getTableColumns(table)) : [],
3209
+ primaryKeys: [],
3210
+ uniqueConstraints: []
2870
3211
  };
2871
3212
  }
2872
3213
  },
@@ -2985,7 +3326,8 @@ export const r = rumble({
2985
3326
  const { makePubSubInstance, pubsub } = createPubSubInstance({ ...rumbleInput });
2986
3327
  const { schemaBuilder } = createSchemaBuilder({
2987
3328
  ...rumbleInput,
2988
- pubsub
3329
+ pubsub,
3330
+ abilityBuilder
2989
3331
  });
2990
3332
  const enum_ = createEnumImplementer({
2991
3333
  ...rumbleInput,
@@ -3249,6 +3591,6 @@ export const r = rumble({
3249
3591
  };
3250
3592
  };
3251
3593
  //#endregion
3252
- export { RumbleError, RumbleErrorSafe, assertFindFirstExists, assertFirstEntryExists, mapNullFieldsToUndefined, rumble };
3594
+ export { RumbleError, RumbleErrorSafe, assertFindFirstExists, assertFirstEntryExists, mapNullFieldsToUndefined, normalizeEmailAddress, normalizePersonName, parsePhoneNumber, rumble };
3253
3595
 
3254
3596
  //# sourceMappingURL=index.mjs.map