@m1212e/rumble 0.25.3 → 0.26.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/README.md +14 -0
- package/out/client/generate.cjs +1 -1
- package/out/client/generate.d.cts.map +1 -1
- package/out/client/generate.d.mts.map +1 -1
- package/out/client/generate.mjs +1 -1
- package/out/client.cjs +9 -4
- package/out/client.cjs.map +1 -1
- package/out/client.mjs +9 -4
- package/out/client.mjs.map +1 -1
- package/out/{generate-BAZg-eXa.mjs → generate-BySnFVve.mjs} +9 -3
- package/out/{generate-BAZg-eXa.mjs.map → generate-BySnFVve.mjs.map} +1 -1
- package/out/{generate-Ci6AXNnn.cjs → generate-D0CKBths.cjs} +8 -2
- package/out/generate-D0CKBths.cjs.map +1 -0
- package/out/index.cjs +361 -165
- package/out/index.cjs.map +1 -1
- package/out/index.d.cts +146 -134
- package/out/index.d.cts.map +1 -1
- package/out/index.d.mts +149 -137
- package/out/index.d.mts.map +1 -1
- package/out/index.mjs +361 -166
- package/out/index.mjs.map +1 -1
- package/package.json +5 -2
- package/out/generate-Ci6AXNnn.cjs.map +0 -1
package/out/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { t as lazy } from "./lazy-C7IyM4M4.mjs";
|
|
2
|
-
import { t as generateFromSchema } from "./generate-
|
|
2
|
+
import { t as generateFromSchema } from "./generate-BySnFVve.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { One, aliasedTable, asc, count, desc, getTableColumns, getTableName, is, isTable, or, relationsFilterToSQL, sql } from "drizzle-orm";
|
|
4
5
|
import { GraphQLError, GraphQLScalarType, GraphQLSchema, Kind, execute as defaultExecute, getOperationAST, print, subscribe as defaultSubscribe } from "graphql";
|
|
5
|
-
import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
|
|
6
|
-
import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
|
|
7
|
-
import { SpanStatusCode, isSpanContextValid, trace } from "@opentelemetry/api";
|
|
8
6
|
import { capitalize, cloneDeep, debounce, merge, toMerged } from "es-toolkit";
|
|
9
|
-
import {
|
|
10
|
-
import { One, aliasedTable, count, getTableColumns, getTableName, is, isTable, or, relationsFilterToSQL, sql } from "drizzle-orm";
|
|
7
|
+
import { SpanStatusCode, isSpanContextValid, trace } from "@opentelemetry/api";
|
|
11
8
|
import { ATTR_GRAPHQL_DOCUMENT, ATTR_GRAPHQL_OPERATION_NAME, ATTR_GRAPHQL_OPERATION_TYPE } from "@opentelemetry/semantic-conventions/incubating";
|
|
12
9
|
import { AttributeNames, SpanNames, createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
|
|
10
|
+
import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
|
|
11
|
+
import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
|
|
12
|
+
import { createPubSub, createYoga } from "graphql-yoga";
|
|
13
13
|
import { toCamelCase } from "drizzle-orm/casing";
|
|
14
14
|
import { PgColumn, PgEnumColumn, PgEnumObjectColumn, PgTable, getTableConfig, isPgEnum } from "drizzle-orm/pg-core";
|
|
15
15
|
import { MySqlEnumColumn, MySqlEnumObjectColumn, MySqlTable, getTableConfig as getTableConfig$1 } from "drizzle-orm/mysql-core";
|
|
@@ -42,152 +42,6 @@ var RumbleError = class extends Error {
|
|
|
42
42
|
*/
|
|
43
43
|
var RumbleErrorSafe = class extends GraphQLError {};
|
|
44
44
|
//#endregion
|
|
45
|
-
//#region lib/helpers/asserts.ts
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
* Helper function to map a drizzle findFirst query result,
|
|
49
|
-
* which may be optional, to a correct drizzle type.
|
|
50
|
-
*
|
|
51
|
-
* @throws RumbleError
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
*
|
|
55
|
-
* ```ts
|
|
56
|
-
* schemaBuilder.queryFields((t) => {
|
|
57
|
-
return {
|
|
58
|
-
findFirstUser: t.drizzleField({
|
|
59
|
-
type: UserRef,
|
|
60
|
-
resolve: (query, root, args, ctx, info) => {
|
|
61
|
-
return (
|
|
62
|
-
db.query.users
|
|
63
|
-
.findFirst({
|
|
64
|
-
...query,
|
|
65
|
-
where: (await ctx.abilities.users.filter("read")).single.where,
|
|
66
|
-
})
|
|
67
|
-
// note that we need to manually raise an error if the value is not found
|
|
68
|
-
.then(assertFindFirstExists)
|
|
69
|
-
);
|
|
70
|
-
},
|
|
71
|
-
}),
|
|
72
|
-
};
|
|
73
|
-
});
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
const assertFindFirstExists = (value) => {
|
|
77
|
-
if (!value) throw new RumbleErrorSafe("Value not found but required (findFirst)");
|
|
78
|
-
return value;
|
|
79
|
-
};
|
|
80
|
-
/**
|
|
81
|
-
*
|
|
82
|
-
* Helper function to map a drizzle findFirst query result,
|
|
83
|
-
* which may be optional, to a correct drizzle type.
|
|
84
|
-
*
|
|
85
|
-
* @throws RumbleError
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
*
|
|
89
|
-
* ```ts
|
|
90
|
-
schemaBuilder.mutationFields((t) => {
|
|
91
|
-
return {
|
|
92
|
-
updateUsername: t.drizzleField({
|
|
93
|
-
type: UserRef,
|
|
94
|
-
args: {
|
|
95
|
-
userId: t.arg.int({ required: true }),
|
|
96
|
-
newName: t.arg.string({ required: true }),
|
|
97
|
-
},
|
|
98
|
-
resolve: (query, root, args, ctx, info) => {
|
|
99
|
-
return db
|
|
100
|
-
.update(schema.users)
|
|
101
|
-
.set({
|
|
102
|
-
name: args.newName,
|
|
103
|
-
})
|
|
104
|
-
.where(
|
|
105
|
-
and(
|
|
106
|
-
eq(schema.users.id, args.userId),
|
|
107
|
-
(await ctx.abilities.users.filter("update")).single.where
|
|
108
|
-
)
|
|
109
|
-
)
|
|
110
|
-
.returning({ id: schema.users.id, name: schema.users.name })
|
|
111
|
-
// note that we need to manually raise an error if the value is not found
|
|
112
|
-
.then(assertFirstEntryExists);
|
|
113
|
-
},
|
|
114
|
-
}),
|
|
115
|
-
};
|
|
116
|
-
});
|
|
117
|
-
* ```
|
|
118
|
-
*/
|
|
119
|
-
const assertFirstEntryExists = (value) => {
|
|
120
|
-
const v = value.at(0);
|
|
121
|
-
if (!v) throw new RumbleErrorSafe("Value not found but required (firstEntry)");
|
|
122
|
-
return v;
|
|
123
|
-
};
|
|
124
|
-
//#endregion
|
|
125
|
-
//#region lib/helpers/mapNullFieldsToUndefined.ts
|
|
126
|
-
/**
|
|
127
|
-
* Helper to map null fields to undefined
|
|
128
|
-
* @param obj The object to map
|
|
129
|
-
* @returns The mapped object with all fields of 'null' transformed to 'undefined'
|
|
130
|
-
*
|
|
131
|
-
* This becomes useful for update mutations where you do not want to pass null (unset value in db)
|
|
132
|
-
* but undefined (do not touch value in db) in case of a value not beeing set in the args of said mutation
|
|
133
|
-
* @example
|
|
134
|
-
* ```ts
|
|
135
|
-
* updateUser: t.drizzleField({
|
|
136
|
-
type: User,
|
|
137
|
-
args: {
|
|
138
|
-
id: t.arg.string({ required: true }),
|
|
139
|
-
email: t.arg.string(),
|
|
140
|
-
lastName: t.arg.string(),
|
|
141
|
-
firstName: t.arg.string(),
|
|
142
|
-
},
|
|
143
|
-
resolve: async (query, root, args, ctx, info) => {
|
|
144
|
-
const mappedArgs = mapNullFieldsToUndefined(args);
|
|
145
|
-
const user = await db.transaction(async (tx) => {
|
|
146
|
-
const user = await tx
|
|
147
|
-
.update(schema.user)
|
|
148
|
-
.set({
|
|
149
|
-
|
|
150
|
-
// email: args.email ?? undefined,
|
|
151
|
-
// lastName: args.lastName ?? undefined,
|
|
152
|
-
// firstName: args.firstName ?? undefined,
|
|
153
|
-
|
|
154
|
-
// becomes this
|
|
155
|
-
|
|
156
|
-
email: mappedArgs.email,
|
|
157
|
-
lastName: mappedArgs.lastName,
|
|
158
|
-
firstName: mappedArgs.firstName,
|
|
159
|
-
})
|
|
160
|
-
.returning()
|
|
161
|
-
.then(assertFirstEntryExists);
|
|
162
|
-
return user;
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
pubsub.updated(user.id);
|
|
166
|
-
|
|
167
|
-
return db.query.user
|
|
168
|
-
.findFirst(
|
|
169
|
-
query(
|
|
170
|
-
(await ctx.abilities.user.filter('read')).merge(
|
|
171
|
-
{
|
|
172
|
-
where: { id: user.id },
|
|
173
|
-
}
|
|
174
|
-
1).query.single,
|
|
175
|
-
),
|
|
176
|
-
)
|
|
177
|
-
.then(assertFindFirstExists);
|
|
178
|
-
},
|
|
179
|
-
}),
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* ```
|
|
183
|
-
*/
|
|
184
|
-
function mapNullFieldsToUndefined(obj) {
|
|
185
|
-
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
|
|
186
|
-
}
|
|
187
|
-
//#endregion
|
|
188
|
-
//#region package.json
|
|
189
|
-
var version = "0.25.3";
|
|
190
|
-
//#endregion
|
|
191
45
|
//#region lib/helpers/tableHelpers.ts
|
|
192
46
|
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
193
47
|
function buildIndex(db) {
|
|
@@ -321,6 +175,8 @@ function sanitizeFilterValue(value) {
|
|
|
321
175
|
const ACTION_FLAG_KEY = "__rumble_action";
|
|
322
176
|
const columnFlagKey = (group) => `__rumble_columns_${group}`;
|
|
323
177
|
const columnMaskKey = Symbol.for("rumble:columnMask");
|
|
178
|
+
const columnVisibilityKey = Symbol.for("rumble:columnVisibility");
|
|
179
|
+
const guardInputKey = Symbol.for("rumble:guardInput");
|
|
324
180
|
const resolvedFilterKey = Symbol.for("rumble:resolvedFilter");
|
|
325
181
|
const requestStateKey = Symbol.for("rumble:requestState");
|
|
326
182
|
//#endregion
|
|
@@ -328,6 +184,7 @@ const requestStateKey = Symbol.for("rumble:requestState");
|
|
|
328
184
|
function createRequestState() {
|
|
329
185
|
return {
|
|
330
186
|
masked: /* @__PURE__ */ new WeakMap(),
|
|
187
|
+
hidden: /* @__PURE__ */ new WeakMap(),
|
|
331
188
|
actions: /* @__PURE__ */ new Map(),
|
|
332
189
|
filterLoaders: /* @__PURE__ */ new Map()
|
|
333
190
|
};
|
|
@@ -410,7 +267,7 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
|
410
267
|
const toSQL = (table, wheres) => or(...wheres.map((where) => relationsFilterToSQL(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)));
|
|
411
268
|
const uncorrelated = (table, wheres, group) => {
|
|
412
269
|
const inner = aliasedTable(tableSchema.table, `rumble_columns_${group}`);
|
|
413
|
-
return sql
|
|
270
|
+
return sql`exists ${db.select({ pk: inner[primaryKeyName] }).from(inner).where(sql`${inner[primaryKeyName]} = ${table[primaryKeyName]} and (${toSQL(inner, wheres)})`)}`;
|
|
414
271
|
};
|
|
415
272
|
return function resolveColumnAccess(action, filters) {
|
|
416
273
|
const rules = filters.map((f) => ({
|
|
@@ -424,7 +281,14 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
|
424
281
|
if (guaranteed.size === allColumnNames.length) return {
|
|
425
282
|
columns,
|
|
426
283
|
extras: void 0,
|
|
427
|
-
mask: void 0
|
|
284
|
+
mask: void 0,
|
|
285
|
+
visibility: void 0
|
|
286
|
+
};
|
|
287
|
+
const visibility = (column) => {
|
|
288
|
+
if (guaranteed.has(column)) return true;
|
|
289
|
+
const wheres = rules.filter((rule) => rule.columns.has(column)).map((rule) => rule.where);
|
|
290
|
+
if (wheres.length === 0) return false;
|
|
291
|
+
return wheres.length === 1 ? wheres[0] : { OR: wheres };
|
|
428
292
|
};
|
|
429
293
|
const mask = {
|
|
430
294
|
guaranteed,
|
|
@@ -444,7 +308,8 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
|
444
308
|
return {
|
|
445
309
|
columns,
|
|
446
310
|
extras,
|
|
447
|
-
mask
|
|
311
|
+
mask,
|
|
312
|
+
visibility
|
|
448
313
|
};
|
|
449
314
|
};
|
|
450
315
|
}
|
|
@@ -503,12 +368,22 @@ function createColumnMasker({ actions }, tables) {
|
|
|
503
368
|
const hidden = hiddenColumns(mask, matchedGroups(row, mask.flags), columnNames, primaryKey);
|
|
504
369
|
maskRow(row, mask, hidden);
|
|
505
370
|
state.masked.set(row, hidden.primaryKey);
|
|
371
|
+
state.hidden.set(row, hidden);
|
|
506
372
|
}
|
|
507
373
|
};
|
|
374
|
+
/**
|
|
375
|
+
* Whether masking hid any of the columns on the row. A hidden primary key
|
|
376
|
+
* counts for the columns it consists of.
|
|
377
|
+
*/
|
|
378
|
+
const columnsHidden = (abilities, row, columns, primaryKey) => {
|
|
379
|
+
const hidden = stateOf(abilities).hidden.get(row);
|
|
380
|
+
return !!hidden && columns.some((column) => hidden.cleared.includes(column) || hidden.primaryKey && primaryKey.includes(column));
|
|
381
|
+
};
|
|
508
382
|
return {
|
|
509
383
|
maskColumns,
|
|
510
384
|
maskedActions,
|
|
511
|
-
primaryKeyHidden
|
|
385
|
+
primaryKeyHidden,
|
|
386
|
+
columnsHidden
|
|
512
387
|
};
|
|
513
388
|
}
|
|
514
389
|
//#endregion
|
|
@@ -889,10 +764,11 @@ function createFilterResponder({ db, defaultLimit }, tableSchema) {
|
|
|
889
764
|
/**
|
|
890
765
|
* Packs the filters into a response object that can be applied for queries by the user
|
|
891
766
|
*/
|
|
892
|
-
return function transformToResponse(queryFilters, columnMask) {
|
|
767
|
+
return function transformToResponse(queryFilters, columnMask, columnVisibility) {
|
|
893
768
|
const ret = internalTransformer(queryFilters);
|
|
894
769
|
ret.merge = (filters) => internalTransformer(mergeFilters(ret.query.many, filters), filters.limit);
|
|
895
770
|
ret[columnMaskKey] = columnMask;
|
|
771
|
+
ret[columnVisibilityKey] = columnVisibility;
|
|
896
772
|
return ret;
|
|
897
773
|
};
|
|
898
774
|
}
|
|
@@ -1072,7 +948,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
|
1072
948
|
const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
|
|
1073
949
|
return mergeFilters(a, b, "OR");
|
|
1074
950
|
});
|
|
1075
|
-
const { columns, extras, mask } = resolveColumnAccess(action, allQueryFilters);
|
|
951
|
+
const { columns, extras, mask, visibility } = resolveColumnAccess(action, allQueryFilters);
|
|
1076
952
|
if (columns) mergedFilters.columns = columns;
|
|
1077
953
|
else delete mergedFilters.columns;
|
|
1078
954
|
if (extras) mergedFilters.extras = {
|
|
@@ -1080,7 +956,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
|
1080
956
|
...extras
|
|
1081
957
|
};
|
|
1082
958
|
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
1083
|
-
return transformToResponse(mergedFilters, mask);
|
|
959
|
+
return transformToResponse(mergedFilters, mask, visibility);
|
|
1084
960
|
};
|
|
1085
961
|
return { withContext: (userContext) => {
|
|
1086
962
|
const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(userContext, action, attributes));
|
|
@@ -1100,6 +976,140 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
|
1100
976
|
} };
|
|
1101
977
|
};
|
|
1102
978
|
//#endregion
|
|
979
|
+
//#region lib/abilityBuilder/inputGuard.ts
|
|
980
|
+
/** The ts names of the columns a relation joins on, on either side. */
|
|
981
|
+
function relationColumnKeys(db, table, columns) {
|
|
982
|
+
const known = tableHelper({
|
|
983
|
+
db,
|
|
984
|
+
table
|
|
985
|
+
}).columns;
|
|
986
|
+
return (columns ?? []).flatMap((column) => {
|
|
987
|
+
const raw = column?._?.column ?? column;
|
|
988
|
+
const key = Object.entries(known).find(([name, candidate]) => candidate === raw || name === raw?.name)?.[0];
|
|
989
|
+
return key ? [key] : [];
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
993
|
+
/**
|
|
994
|
+
* Column masking hides values after the query ran, but a `where` or `orderBy`
|
|
995
|
+
* argument is applied by the database on the real values: filtering for
|
|
996
|
+
* `{ salary: { gt: 5000 } }` reveals which rows exceed it whether or not the
|
|
997
|
+
* column may be read. This restricts such arguments to what the abilities
|
|
998
|
+
* expose:
|
|
999
|
+
*
|
|
1000
|
+
* - a column only matches on rows which expose it, for all other rows the
|
|
1001
|
+
* condition is false
|
|
1002
|
+
* - sorting by a column treats it as empty on rows which do not expose it
|
|
1003
|
+
* - a relation filter requires the columns the relation joins on to be
|
|
1004
|
+
* exposed, and is restricted to the rows the caller may read of the related
|
|
1005
|
+
* table, whose masked columns follow the same rules
|
|
1006
|
+
*/
|
|
1007
|
+
function createInputGuard({ db, abilities, readActionOf }) {
|
|
1008
|
+
const visibilityOf = async (table, action) => (await abilities[table].filter(action))[columnVisibilityKey];
|
|
1009
|
+
const never = (table) => createBlockEverythingFilter(table, tableHelper({
|
|
1010
|
+
db,
|
|
1011
|
+
table
|
|
1012
|
+
})).where;
|
|
1013
|
+
/** The conditions under which the given columns are exposed. */
|
|
1014
|
+
const exposed = async (table, action, keys) => {
|
|
1015
|
+
const visibility = await visibilityOf(table, action);
|
|
1016
|
+
if (!visibility) return [];
|
|
1017
|
+
const conditions = [];
|
|
1018
|
+
for (const key of keys) {
|
|
1019
|
+
const condition = visibility(key);
|
|
1020
|
+
if (condition === true) continue;
|
|
1021
|
+
conditions.push(condition === false ? never(table) : condition);
|
|
1022
|
+
}
|
|
1023
|
+
return conditions;
|
|
1024
|
+
};
|
|
1025
|
+
/** The filter of a relation: the related rows the caller may read, exposing what it joins on. */
|
|
1026
|
+
const guardRelation = async (table, action, key, value, guarded, conditions) => {
|
|
1027
|
+
const relation = tableHelper({
|
|
1028
|
+
db,
|
|
1029
|
+
table
|
|
1030
|
+
}).relations[key];
|
|
1031
|
+
const target = relation.targetTableName;
|
|
1032
|
+
const targetAction = readActionOf(target);
|
|
1033
|
+
const targetKeys = relationColumnKeys(db, target, relation.targetColumns);
|
|
1034
|
+
conditions.push(...await exposed(table, action, relationColumnKeys(db, table, relation.sourceColumns)));
|
|
1035
|
+
const readable = (await abilities[target].filter(targetAction)).query.many.where;
|
|
1036
|
+
const related = [...readable ? [readable] : [], ...await exposed(target, targetAction, targetKeys)];
|
|
1037
|
+
const inner = isObject(value) ? await guardWhere(target, targetAction, value) : {};
|
|
1038
|
+
if (related.length === 0) guarded[key] = isObject(value) ? inner : value;
|
|
1039
|
+
else if (value === false) conditions.push({ NOT: { [key]: { AND: related } } });
|
|
1040
|
+
else guarded[key] = {
|
|
1041
|
+
...inner,
|
|
1042
|
+
AND: [...inner.AND ?? [], ...related]
|
|
1043
|
+
};
|
|
1044
|
+
};
|
|
1045
|
+
const guardLogic = (table, action, value) => Array.isArray(value) ? Promise.all(value.map((v) => guardWhere(table, action, v))) : guardWhere(table, action, value);
|
|
1046
|
+
/** A single entry of a where: a logical operator, a relation or a column. */
|
|
1047
|
+
const guardEntry = async (table, action, [key, value], guarded, conditions) => {
|
|
1048
|
+
const schema = tableHelper({
|
|
1049
|
+
db,
|
|
1050
|
+
table
|
|
1051
|
+
});
|
|
1052
|
+
if (key === "AND" || key === "OR" || key === "NOT") guarded[key] = await guardLogic(table, action, value);
|
|
1053
|
+
else if (key in schema.relations) await guardRelation(table, action, key, value, guarded, conditions);
|
|
1054
|
+
else {
|
|
1055
|
+
guarded[key] = value;
|
|
1056
|
+
if (key in schema.columns) conditions.push(...await exposed(table, action, [key]));
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
const guardWhere = async (table, action, where) => {
|
|
1060
|
+
if (!isObject(where)) return where;
|
|
1061
|
+
const guarded = {};
|
|
1062
|
+
const conditions = [];
|
|
1063
|
+
for (const entry of Object.entries(where)) await guardEntry(table, action, entry, guarded, conditions);
|
|
1064
|
+
if (conditions.length === 0) return guarded;
|
|
1065
|
+
return {
|
|
1066
|
+
...guarded,
|
|
1067
|
+
AND: [...guarded.AND ?? [], ...conditions]
|
|
1068
|
+
};
|
|
1069
|
+
};
|
|
1070
|
+
const guardOrderBy = async (table, action, orderBy) => {
|
|
1071
|
+
if (!isObject(orderBy)) return orderBy;
|
|
1072
|
+
const visibility = await visibilityOf(table, action);
|
|
1073
|
+
if (!visibility) return orderBy;
|
|
1074
|
+
const entries = Object.entries(orderBy).filter(([, direction]) => direction);
|
|
1075
|
+
if (entries.every(([key]) => visibility(key) === true)) return orderBy;
|
|
1076
|
+
const schema = tableHelper({
|
|
1077
|
+
db,
|
|
1078
|
+
table
|
|
1079
|
+
});
|
|
1080
|
+
return (aliased, { asc, desc }) => entries.map(([key, direction]) => {
|
|
1081
|
+
const condition = visibility(key);
|
|
1082
|
+
const column = aliased[key];
|
|
1083
|
+
const value = condition === true ? column : condition === false ? null : sql`case when ${relationsFilterToSQL(aliased, sanitizeFilterValue(condition), schema.relations, db._.relations)} then ${column} end`;
|
|
1084
|
+
return (direction === "asc" ? asc : desc)(value ?? sql`null`);
|
|
1085
|
+
});
|
|
1086
|
+
};
|
|
1087
|
+
return {
|
|
1088
|
+
input: async (table, action, input) => ({
|
|
1089
|
+
...input,
|
|
1090
|
+
where: await guardWhere(table, action, input.where),
|
|
1091
|
+
orderBy: await guardOrderBy(table, action, input.orderBy)
|
|
1092
|
+
}),
|
|
1093
|
+
exposed
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Restricts client supplied `where` and `orderBy` arguments to what the
|
|
1098
|
+
* abilities of the request let the caller read, see `createInputGuard`.
|
|
1099
|
+
*/
|
|
1100
|
+
function guardFilterInput(abilities, table, action, input) {
|
|
1101
|
+
const guard = abilities[guardInputKey];
|
|
1102
|
+
return guard ? guard.input(table, action, input) : Promise.resolve(input);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* The conditions a row of the related table has to meet to be reached through
|
|
1106
|
+
* a relation: the columns the relation joins on have to be exposed on it.
|
|
1107
|
+
*/
|
|
1108
|
+
async function relationExposure(abilities, table, action, columns) {
|
|
1109
|
+
const guard = abilities[guardInputKey];
|
|
1110
|
+
return guard ? guard.exposed(table, action, columns) : [];
|
|
1111
|
+
}
|
|
1112
|
+
//#endregion
|
|
1103
1113
|
//#region lib/abilityBuilder/tableBuilder.ts
|
|
1104
1114
|
/**
|
|
1105
1115
|
* Creates the registration API (allow/filter) of a single table.
|
|
@@ -1210,7 +1220,7 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
|
|
|
1210
1220
|
actions: settings.actions,
|
|
1211
1221
|
isBuilt: () => hasBeenBuilt
|
|
1212
1222
|
})]));
|
|
1213
|
-
const { maskColumns, maskedActions, primaryKeyHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
|
|
1223
|
+
const { maskColumns, maskedActions, primaryKeyHidden, columnsHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
|
|
1214
1224
|
const tableSchema = tableHelper({
|
|
1215
1225
|
db,
|
|
1216
1226
|
table: tableName
|
|
@@ -1233,6 +1243,8 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
|
|
|
1233
1243
|
maskedActions: (abilities, table) => maskedActions(abilities, table),
|
|
1234
1244
|
/** Whether masking in this request hid the primary key of the row. */
|
|
1235
1245
|
primaryKeyHidden: (abilities, row) => primaryKeyHidden(abilities, row),
|
|
1246
|
+
/** Whether masking in this request hid one of the columns on the row. */
|
|
1247
|
+
columnsHidden: (abilities, row, columns, primaryKey) => columnsHidden(abilities, row, columns, primaryKey),
|
|
1236
1248
|
registerReadAction(table, action) {
|
|
1237
1249
|
readActionPerTable.set(table, action);
|
|
1238
1250
|
},
|
|
@@ -1249,6 +1261,11 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
|
|
|
1249
1261
|
const state = createRequestState();
|
|
1250
1262
|
const abilities = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
|
|
1251
1263
|
attachRequestState(abilities, state);
|
|
1264
|
+
Object.defineProperty(abilities, guardInputKey, { value: createInputGuard({
|
|
1265
|
+
db,
|
|
1266
|
+
abilities,
|
|
1267
|
+
readActionOf: (table) => readActionPerTable.get(table) ?? "read"
|
|
1268
|
+
}) });
|
|
1252
1269
|
return abilities;
|
|
1253
1270
|
};
|
|
1254
1271
|
}
|
|
@@ -1256,6 +1273,152 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
|
|
|
1256
1273
|
};
|
|
1257
1274
|
};
|
|
1258
1275
|
//#endregion
|
|
1276
|
+
//#region lib/helpers/asserts.ts
|
|
1277
|
+
/**
|
|
1278
|
+
*
|
|
1279
|
+
* Helper function to map a drizzle findFirst query result,
|
|
1280
|
+
* which may be optional, to a correct drizzle type.
|
|
1281
|
+
*
|
|
1282
|
+
* @throws RumbleError
|
|
1283
|
+
*
|
|
1284
|
+
* @example
|
|
1285
|
+
*
|
|
1286
|
+
* ```ts
|
|
1287
|
+
* schemaBuilder.queryFields((t) => {
|
|
1288
|
+
return {
|
|
1289
|
+
findFirstUser: t.drizzleField({
|
|
1290
|
+
type: UserRef,
|
|
1291
|
+
resolve: (query, root, args, ctx, info) => {
|
|
1292
|
+
return (
|
|
1293
|
+
db.query.users
|
|
1294
|
+
.findFirst({
|
|
1295
|
+
...query,
|
|
1296
|
+
where: (await ctx.abilities.users.filter("read")).single.where,
|
|
1297
|
+
})
|
|
1298
|
+
// note that we need to manually raise an error if the value is not found
|
|
1299
|
+
.then(assertFindFirstExists)
|
|
1300
|
+
);
|
|
1301
|
+
},
|
|
1302
|
+
}),
|
|
1303
|
+
};
|
|
1304
|
+
});
|
|
1305
|
+
* ```
|
|
1306
|
+
*/
|
|
1307
|
+
const assertFindFirstExists = (value) => {
|
|
1308
|
+
if (!value) throw new RumbleErrorSafe("Value not found but required (findFirst)");
|
|
1309
|
+
return value;
|
|
1310
|
+
};
|
|
1311
|
+
/**
|
|
1312
|
+
*
|
|
1313
|
+
* Helper function to map a drizzle findFirst query result,
|
|
1314
|
+
* which may be optional, to a correct drizzle type.
|
|
1315
|
+
*
|
|
1316
|
+
* @throws RumbleError
|
|
1317
|
+
*
|
|
1318
|
+
* @example
|
|
1319
|
+
*
|
|
1320
|
+
* ```ts
|
|
1321
|
+
schemaBuilder.mutationFields((t) => {
|
|
1322
|
+
return {
|
|
1323
|
+
updateUsername: t.drizzleField({
|
|
1324
|
+
type: UserRef,
|
|
1325
|
+
args: {
|
|
1326
|
+
userId: t.arg.int({ required: true }),
|
|
1327
|
+
newName: t.arg.string({ required: true }),
|
|
1328
|
+
},
|
|
1329
|
+
resolve: (query, root, args, ctx, info) => {
|
|
1330
|
+
return db
|
|
1331
|
+
.update(schema.users)
|
|
1332
|
+
.set({
|
|
1333
|
+
name: args.newName,
|
|
1334
|
+
})
|
|
1335
|
+
.where(
|
|
1336
|
+
and(
|
|
1337
|
+
eq(schema.users.id, args.userId),
|
|
1338
|
+
(await ctx.abilities.users.filter("update")).single.where
|
|
1339
|
+
)
|
|
1340
|
+
)
|
|
1341
|
+
.returning({ id: schema.users.id, name: schema.users.name })
|
|
1342
|
+
// note that we need to manually raise an error if the value is not found
|
|
1343
|
+
.then(assertFirstEntryExists);
|
|
1344
|
+
},
|
|
1345
|
+
}),
|
|
1346
|
+
};
|
|
1347
|
+
});
|
|
1348
|
+
* ```
|
|
1349
|
+
*/
|
|
1350
|
+
const assertFirstEntryExists = (value) => {
|
|
1351
|
+
const v = value.at(0);
|
|
1352
|
+
if (!v) throw new RumbleErrorSafe("Value not found but required (firstEntry)");
|
|
1353
|
+
return v;
|
|
1354
|
+
};
|
|
1355
|
+
//#endregion
|
|
1356
|
+
//#region lib/helpers/mapNullFieldsToUndefined.ts
|
|
1357
|
+
/**
|
|
1358
|
+
* Helper to map null fields to undefined
|
|
1359
|
+
* @param obj The object to map
|
|
1360
|
+
* @returns The mapped object with all fields of 'null' transformed to 'undefined'
|
|
1361
|
+
*
|
|
1362
|
+
* This becomes useful for update mutations where you do not want to pass null (unset value in db)
|
|
1363
|
+
* but undefined (do not touch value in db) in case of a value not beeing set in the args of said mutation
|
|
1364
|
+
* @example
|
|
1365
|
+
* ```ts
|
|
1366
|
+
* updateUser: t.drizzleField({
|
|
1367
|
+
type: User,
|
|
1368
|
+
args: {
|
|
1369
|
+
id: t.arg.string({ required: true }),
|
|
1370
|
+
email: t.arg.string(),
|
|
1371
|
+
lastName: t.arg.string(),
|
|
1372
|
+
firstName: t.arg.string(),
|
|
1373
|
+
},
|
|
1374
|
+
resolve: async (query, root, args, ctx, info) => {
|
|
1375
|
+
const mappedArgs = mapNullFieldsToUndefined(args);
|
|
1376
|
+
const user = await db.transaction(async (tx) => {
|
|
1377
|
+
const user = await tx
|
|
1378
|
+
.update(schema.user)
|
|
1379
|
+
.set({
|
|
1380
|
+
|
|
1381
|
+
// email: args.email ?? undefined,
|
|
1382
|
+
// lastName: args.lastName ?? undefined,
|
|
1383
|
+
// firstName: args.firstName ?? undefined,
|
|
1384
|
+
|
|
1385
|
+
// becomes this
|
|
1386
|
+
|
|
1387
|
+
email: mappedArgs.email,
|
|
1388
|
+
lastName: mappedArgs.lastName,
|
|
1389
|
+
firstName: mappedArgs.firstName,
|
|
1390
|
+
})
|
|
1391
|
+
.returning()
|
|
1392
|
+
.then(assertFirstEntryExists);
|
|
1393
|
+
return user;
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
pubsub.updated(user.id);
|
|
1397
|
+
|
|
1398
|
+
return db.query.user
|
|
1399
|
+
.findFirst(
|
|
1400
|
+
query(
|
|
1401
|
+
(await ctx.abilities.user.filter('read')).merge(
|
|
1402
|
+
{
|
|
1403
|
+
where: { id: user.id },
|
|
1404
|
+
}
|
|
1405
|
+
1).query.single,
|
|
1406
|
+
),
|
|
1407
|
+
)
|
|
1408
|
+
.then(assertFindFirstExists);
|
|
1409
|
+
},
|
|
1410
|
+
}),
|
|
1411
|
+
*
|
|
1412
|
+
*
|
|
1413
|
+
* ```
|
|
1414
|
+
*/
|
|
1415
|
+
function mapNullFieldsToUndefined(obj) {
|
|
1416
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
|
|
1417
|
+
}
|
|
1418
|
+
//#endregion
|
|
1419
|
+
//#region package.json
|
|
1420
|
+
var version = "0.26.2";
|
|
1421
|
+
//#endregion
|
|
1259
1422
|
//#region lib/args/orderArg.ts
|
|
1260
1423
|
const makeDefaultName$1 = (dbName) => `${capitalize(toCamelCase(dbName.toString()))}OrderInputArgument`;
|
|
1261
1424
|
const createOrderArgImplementer = ({ db, schemaBuilder }) => {
|
|
@@ -1682,6 +1845,8 @@ const createCountQueryImplementer = ({ db, schemaBuilder, whereArgImplementer, m
|
|
|
1682
1845
|
resolve: async (_root, args, ctx, _info) => {
|
|
1683
1846
|
if (isAllowed && !await isAllowed(ctx)) throw new RumbleErrorSafe("Not allowed to perform this action");
|
|
1684
1847
|
deepSetProto(args);
|
|
1848
|
+
const guarded = await guardFilterInput(ctx.abilities, String(table), listAction, { where: args.where });
|
|
1849
|
+
if (args.where) args.where = guarded.where;
|
|
1685
1850
|
return db.select({ count: count() }).from(tableSchema.table).where((await ctx.abilities[table].filter(listAction)).merge(mapNullFieldsToUndefined(args)).sql.where).then(assertFirstEntryExists).then((r) => r.count);
|
|
1686
1851
|
}
|
|
1687
1852
|
}) };
|
|
@@ -2079,9 +2244,12 @@ function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
|
|
|
2079
2244
|
args.extras = { search_distance: (table) => sql`${sql.join(columnsToSearch.map(([key]) => {
|
|
2080
2245
|
return sql`COALESCE((${table[key]}::TEXT <-> ${searchParam}), 1)`;
|
|
2081
2246
|
}), sql.raw(" + "))}` };
|
|
2082
|
-
const originalOrderBy = cloneDeep(args.orderBy);
|
|
2247
|
+
const originalOrderBy = typeof args.orderBy === "function" ? args.orderBy : cloneDeep(args.orderBy);
|
|
2083
2248
|
args.orderBy = (table) => {
|
|
2084
|
-
const argsOrderBySQL = sql.join(
|
|
2249
|
+
const argsOrderBySQL = sql.join(typeof originalOrderBy === "function" ? originalOrderBy(table, {
|
|
2250
|
+
asc,
|
|
2251
|
+
desc
|
|
2252
|
+
}) : Object.entries(originalOrderBy ?? {}).map(([key, value]) => {
|
|
2085
2253
|
if (value === "asc") return sql`${table[key]} ASC`;
|
|
2086
2254
|
else if (value === "desc") return sql`${table[key]} DESC`;
|
|
2087
2255
|
else throw new Error(`Invalid value ${value} for orderBy`);
|
|
@@ -2165,6 +2333,13 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
2165
2333
|
fields: Object.keys(tableSchema.primaryKey),
|
|
2166
2334
|
isHidden: (context, row) => abilityBuilder._.primaryKeyHidden(context.abilities, row)
|
|
2167
2335
|
},
|
|
2336
|
+
maskRelations: {
|
|
2337
|
+
fields: Object.fromEntries(Object.entries(tableSchema.relations ?? {}).map(([key, relation]) => [key, {
|
|
2338
|
+
many: !is(relation, One),
|
|
2339
|
+
sourceColumns: relationColumnKeys(db, tableSchema.tsName, relation.sourceColumns)
|
|
2340
|
+
}])),
|
|
2341
|
+
isHidden: (context, row, columns) => abilityBuilder._.columnsHidden(context.abilities, row, columns, Object.keys(tableSchema.primaryKey))
|
|
2342
|
+
},
|
|
2168
2343
|
fields: (t) => {
|
|
2169
2344
|
const columns = tableSchema.columns;
|
|
2170
2345
|
const configMap = /* @__PURE__ */ new Map();
|
|
@@ -2231,7 +2406,7 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
2231
2406
|
let filterSpecifier = "many";
|
|
2232
2407
|
if (is(value, One)) {
|
|
2233
2408
|
isMany = false;
|
|
2234
|
-
nullable = value.optional || !value.sourceColumns.every((column) => column.notNull);
|
|
2409
|
+
nullable = value.optional || !value.sourceColumns.every((column) => (column._?.column ?? column).notNull);
|
|
2235
2410
|
filterSpecifier = "single";
|
|
2236
2411
|
}
|
|
2237
2412
|
const subscribe = (subscriptions, _element) => {
|
|
@@ -2274,14 +2449,21 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
2274
2449
|
description: `Get the ${pluralize.plural(relationSchema.tsName)} related to this ${pluralize.singular(tableSchema.tsName)}`,
|
|
2275
2450
|
query: async (args, ctx) => {
|
|
2276
2451
|
args = JSON.parse(JSON.stringify(args));
|
|
2452
|
+
const guarded = await guardFilterInput(ctx.abilities, relationSchema.tsName, readAction, {
|
|
2453
|
+
where: args.where,
|
|
2454
|
+
orderBy: args.orderBy
|
|
2455
|
+
});
|
|
2456
|
+
if (args.where) args.where = guarded.where;
|
|
2457
|
+
if (args.orderBy) args.orderBy = guarded.orderBy;
|
|
2277
2458
|
if (isMany) adjustQueryArgsForSearch({
|
|
2278
2459
|
search,
|
|
2279
2460
|
args,
|
|
2280
2461
|
tableSchema: relationSchema,
|
|
2281
2462
|
abilities: await ctx.abilities[relationSchema.tsName].filter(readAction)
|
|
2282
2463
|
});
|
|
2464
|
+
const joined = await relationExposure(ctx.abilities, relationSchema.tsName, readAction, relationColumnKeys(db, relationSchema.tsName, value.targetColumns));
|
|
2283
2465
|
const filter = (await ctx.abilities[relationSchema.tsName].filter(readAction)).merge({
|
|
2284
|
-
where: args.where,
|
|
2466
|
+
where: joined.length ? { AND: [...args.where ? [args.where] : [], ...joined] } : args.where,
|
|
2285
2467
|
limit: args.limit,
|
|
2286
2468
|
extras: args.extras
|
|
2287
2469
|
}).query[filterSpecifier];
|
|
@@ -2443,6 +2625,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2443
2625
|
args: manyArgs,
|
|
2444
2626
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
2445
2627
|
deepSetProto(args);
|
|
2628
|
+
const guarded = await guardFilterInput(ctx.abilities, String(table), listAction, {
|
|
2629
|
+
where: args.where,
|
|
2630
|
+
orderBy: args.orderBy
|
|
2631
|
+
});
|
|
2632
|
+
if (args.where) args.where = guarded.where;
|
|
2633
|
+
if (args.orderBy) args.orderBy = guarded.orderBy;
|
|
2446
2634
|
adjustQueryArgsForSearch({
|
|
2447
2635
|
search,
|
|
2448
2636
|
args,
|
|
@@ -2770,12 +2958,19 @@ function registerPluginOnce(name, plugin) {
|
|
|
2770
2958
|
const maskColumnsKey = "maskColumns";
|
|
2771
2959
|
const maskColumnsActionKey = "maskColumnsAction";
|
|
2772
2960
|
const maskPrimaryKeysKey = "maskPrimaryKeys";
|
|
2961
|
+
const maskRelationsKey = "maskRelations";
|
|
2773
2962
|
const columnMaskPluginName = "ColumnMaskPlugin";
|
|
2774
2963
|
//#endregion
|
|
2775
2964
|
//#region lib/columnMaskPlugin/columnMaskPlugin.ts
|
|
2776
2965
|
var ColumnMaskPlugin = class extends BasePlugin {
|
|
2777
2966
|
wrapResolve(resolver, fieldConfig) {
|
|
2778
|
-
return this.maskResult(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig);
|
|
2967
|
+
return this.maskResult(this.maskRelation(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig), fieldConfig);
|
|
2968
|
+
}
|
|
2969
|
+
maskRelation(resolver, fieldConfig) {
|
|
2970
|
+
const relations = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskRelationsKey];
|
|
2971
|
+
const relation = relations?.fields[fieldConfig.name];
|
|
2972
|
+
if (!relations || !relation) return resolver;
|
|
2973
|
+
return (parent, args, context, info) => relations.isHidden(context, parent, relation.sourceColumns) ? relation.many ? [] : null : resolver(parent, args, context, info);
|
|
2779
2974
|
}
|
|
2780
2975
|
maskPrimaryKey(resolver, fieldConfig) {
|
|
2781
2976
|
const primaryKeys = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskPrimaryKeysKey];
|
|
@@ -3591,6 +3786,6 @@ export const r = rumble({
|
|
|
3591
3786
|
};
|
|
3592
3787
|
};
|
|
3593
3788
|
//#endregion
|
|
3594
|
-
export { RumbleError, RumbleErrorSafe, assertFindFirstExists, assertFirstEntryExists, mapNullFieldsToUndefined, normalizeEmailAddress, normalizePersonName, parsePhoneNumber, rumble };
|
|
3789
|
+
export { RumbleError, RumbleErrorSafe, assertFindFirstExists, assertFirstEntryExists, guardFilterInput, mapNullFieldsToUndefined, normalizeEmailAddress, normalizePersonName, parsePhoneNumber, rumble };
|
|
3595
3790
|
|
|
3596
3791
|
//# sourceMappingURL=index.mjs.map
|