@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.cjs
CHANGED
|
@@ -22,16 +22,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
24
|
const require_lazy = require("./lazy-CAN0oaXD.cjs");
|
|
25
|
-
const require_generate = require("./generate-
|
|
25
|
+
const require_generate = require("./generate-D0CKBths.cjs");
|
|
26
|
+
let drizzle_orm = require("drizzle-orm");
|
|
26
27
|
let graphql = require("graphql");
|
|
27
|
-
let _escape_tech_graphql_armor = require("@escape.tech/graphql-armor");
|
|
28
|
-
let _graphql_yoga_plugin_disable_introspection = require("@graphql-yoga/plugin-disable-introspection");
|
|
29
|
-
let _opentelemetry_api = require("@opentelemetry/api");
|
|
30
28
|
let es_toolkit = require("es-toolkit");
|
|
31
|
-
let
|
|
32
|
-
let drizzle_orm = require("drizzle-orm");
|
|
29
|
+
let _opentelemetry_api = require("@opentelemetry/api");
|
|
33
30
|
let _opentelemetry_semantic_conventions_incubating = require("@opentelemetry/semantic-conventions/incubating");
|
|
34
31
|
let _pothos_tracing_opentelemetry = require("@pothos/tracing-opentelemetry");
|
|
32
|
+
let _escape_tech_graphql_armor = require("@escape.tech/graphql-armor");
|
|
33
|
+
let _graphql_yoga_plugin_disable_introspection = require("@graphql-yoga/plugin-disable-introspection");
|
|
34
|
+
let graphql_yoga = require("graphql-yoga");
|
|
35
35
|
let drizzle_orm_casing = require("drizzle-orm/casing");
|
|
36
36
|
let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
|
|
37
37
|
let drizzle_orm_mysql_core = require("drizzle-orm/mysql-core");
|
|
@@ -72,152 +72,6 @@ var RumbleError = class extends Error {
|
|
|
72
72
|
*/
|
|
73
73
|
var RumbleErrorSafe = class extends graphql.GraphQLError {};
|
|
74
74
|
//#endregion
|
|
75
|
-
//#region lib/helpers/asserts.ts
|
|
76
|
-
/**
|
|
77
|
-
*
|
|
78
|
-
* Helper function to map a drizzle findFirst query result,
|
|
79
|
-
* which may be optional, to a correct drizzle type.
|
|
80
|
-
*
|
|
81
|
-
* @throws RumbleError
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
*
|
|
85
|
-
* ```ts
|
|
86
|
-
* schemaBuilder.queryFields((t) => {
|
|
87
|
-
return {
|
|
88
|
-
findFirstUser: t.drizzleField({
|
|
89
|
-
type: UserRef,
|
|
90
|
-
resolve: (query, root, args, ctx, info) => {
|
|
91
|
-
return (
|
|
92
|
-
db.query.users
|
|
93
|
-
.findFirst({
|
|
94
|
-
...query,
|
|
95
|
-
where: (await ctx.abilities.users.filter("read")).single.where,
|
|
96
|
-
})
|
|
97
|
-
// note that we need to manually raise an error if the value is not found
|
|
98
|
-
.then(assertFindFirstExists)
|
|
99
|
-
);
|
|
100
|
-
},
|
|
101
|
-
}),
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
const assertFindFirstExists = (value) => {
|
|
107
|
-
if (!value) throw new RumbleErrorSafe("Value not found but required (findFirst)");
|
|
108
|
-
return value;
|
|
109
|
-
};
|
|
110
|
-
/**
|
|
111
|
-
*
|
|
112
|
-
* Helper function to map a drizzle findFirst query result,
|
|
113
|
-
* which may be optional, to a correct drizzle type.
|
|
114
|
-
*
|
|
115
|
-
* @throws RumbleError
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
*
|
|
119
|
-
* ```ts
|
|
120
|
-
schemaBuilder.mutationFields((t) => {
|
|
121
|
-
return {
|
|
122
|
-
updateUsername: t.drizzleField({
|
|
123
|
-
type: UserRef,
|
|
124
|
-
args: {
|
|
125
|
-
userId: t.arg.int({ required: true }),
|
|
126
|
-
newName: t.arg.string({ required: true }),
|
|
127
|
-
},
|
|
128
|
-
resolve: (query, root, args, ctx, info) => {
|
|
129
|
-
return db
|
|
130
|
-
.update(schema.users)
|
|
131
|
-
.set({
|
|
132
|
-
name: args.newName,
|
|
133
|
-
})
|
|
134
|
-
.where(
|
|
135
|
-
and(
|
|
136
|
-
eq(schema.users.id, args.userId),
|
|
137
|
-
(await ctx.abilities.users.filter("update")).single.where
|
|
138
|
-
)
|
|
139
|
-
)
|
|
140
|
-
.returning({ id: schema.users.id, name: schema.users.name })
|
|
141
|
-
// note that we need to manually raise an error if the value is not found
|
|
142
|
-
.then(assertFirstEntryExists);
|
|
143
|
-
},
|
|
144
|
-
}),
|
|
145
|
-
};
|
|
146
|
-
});
|
|
147
|
-
* ```
|
|
148
|
-
*/
|
|
149
|
-
const assertFirstEntryExists = (value) => {
|
|
150
|
-
const v = value.at(0);
|
|
151
|
-
if (!v) throw new RumbleErrorSafe("Value not found but required (firstEntry)");
|
|
152
|
-
return v;
|
|
153
|
-
};
|
|
154
|
-
//#endregion
|
|
155
|
-
//#region lib/helpers/mapNullFieldsToUndefined.ts
|
|
156
|
-
/**
|
|
157
|
-
* Helper to map null fields to undefined
|
|
158
|
-
* @param obj The object to map
|
|
159
|
-
* @returns The mapped object with all fields of 'null' transformed to 'undefined'
|
|
160
|
-
*
|
|
161
|
-
* This becomes useful for update mutations where you do not want to pass null (unset value in db)
|
|
162
|
-
* but undefined (do not touch value in db) in case of a value not beeing set in the args of said mutation
|
|
163
|
-
* @example
|
|
164
|
-
* ```ts
|
|
165
|
-
* updateUser: t.drizzleField({
|
|
166
|
-
type: User,
|
|
167
|
-
args: {
|
|
168
|
-
id: t.arg.string({ required: true }),
|
|
169
|
-
email: t.arg.string(),
|
|
170
|
-
lastName: t.arg.string(),
|
|
171
|
-
firstName: t.arg.string(),
|
|
172
|
-
},
|
|
173
|
-
resolve: async (query, root, args, ctx, info) => {
|
|
174
|
-
const mappedArgs = mapNullFieldsToUndefined(args);
|
|
175
|
-
const user = await db.transaction(async (tx) => {
|
|
176
|
-
const user = await tx
|
|
177
|
-
.update(schema.user)
|
|
178
|
-
.set({
|
|
179
|
-
|
|
180
|
-
// email: args.email ?? undefined,
|
|
181
|
-
// lastName: args.lastName ?? undefined,
|
|
182
|
-
// firstName: args.firstName ?? undefined,
|
|
183
|
-
|
|
184
|
-
// becomes this
|
|
185
|
-
|
|
186
|
-
email: mappedArgs.email,
|
|
187
|
-
lastName: mappedArgs.lastName,
|
|
188
|
-
firstName: mappedArgs.firstName,
|
|
189
|
-
})
|
|
190
|
-
.returning()
|
|
191
|
-
.then(assertFirstEntryExists);
|
|
192
|
-
return user;
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
pubsub.updated(user.id);
|
|
196
|
-
|
|
197
|
-
return db.query.user
|
|
198
|
-
.findFirst(
|
|
199
|
-
query(
|
|
200
|
-
(await ctx.abilities.user.filter('read')).merge(
|
|
201
|
-
{
|
|
202
|
-
where: { id: user.id },
|
|
203
|
-
}
|
|
204
|
-
1).query.single,
|
|
205
|
-
),
|
|
206
|
-
)
|
|
207
|
-
.then(assertFindFirstExists);
|
|
208
|
-
},
|
|
209
|
-
}),
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* ```
|
|
213
|
-
*/
|
|
214
|
-
function mapNullFieldsToUndefined(obj) {
|
|
215
|
-
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
|
|
216
|
-
}
|
|
217
|
-
//#endregion
|
|
218
|
-
//#region package.json
|
|
219
|
-
var version = "0.25.3";
|
|
220
|
-
//#endregion
|
|
221
75
|
//#region lib/helpers/tableHelpers.ts
|
|
222
76
|
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
223
77
|
function buildIndex(db) {
|
|
@@ -351,6 +205,8 @@ function sanitizeFilterValue(value) {
|
|
|
351
205
|
const ACTION_FLAG_KEY = "__rumble_action";
|
|
352
206
|
const columnFlagKey = (group) => `__rumble_columns_${group}`;
|
|
353
207
|
const columnMaskKey = Symbol.for("rumble:columnMask");
|
|
208
|
+
const columnVisibilityKey = Symbol.for("rumble:columnVisibility");
|
|
209
|
+
const guardInputKey = Symbol.for("rumble:guardInput");
|
|
354
210
|
const resolvedFilterKey = Symbol.for("rumble:resolvedFilter");
|
|
355
211
|
const requestStateKey = Symbol.for("rumble:requestState");
|
|
356
212
|
//#endregion
|
|
@@ -358,6 +214,7 @@ const requestStateKey = Symbol.for("rumble:requestState");
|
|
|
358
214
|
function createRequestState() {
|
|
359
215
|
return {
|
|
360
216
|
masked: /* @__PURE__ */ new WeakMap(),
|
|
217
|
+
hidden: /* @__PURE__ */ new WeakMap(),
|
|
361
218
|
actions: /* @__PURE__ */ new Map(),
|
|
362
219
|
filterLoaders: /* @__PURE__ */ new Map()
|
|
363
220
|
};
|
|
@@ -440,7 +297,7 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
|
440
297
|
const toSQL = (table, wheres) => (0, drizzle_orm.or)(...wheres.map((where) => (0, drizzle_orm.relationsFilterToSQL)(table, sanitizeFilterValue(where), tableSchema.relations, db._.relations)));
|
|
441
298
|
const uncorrelated = (table, wheres, group) => {
|
|
442
299
|
const inner = (0, drizzle_orm.aliasedTable)(tableSchema.table, `rumble_columns_${group}`);
|
|
443
|
-
return drizzle_orm.sql
|
|
300
|
+
return drizzle_orm.sql`exists ${db.select({ pk: inner[primaryKeyName] }).from(inner).where(drizzle_orm.sql`${inner[primaryKeyName]} = ${table[primaryKeyName]} and (${toSQL(inner, wheres)})`)}`;
|
|
444
301
|
};
|
|
445
302
|
return function resolveColumnAccess(action, filters) {
|
|
446
303
|
const rules = filters.map((f) => ({
|
|
@@ -454,7 +311,14 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
|
454
311
|
if (guaranteed.size === allColumnNames.length) return {
|
|
455
312
|
columns,
|
|
456
313
|
extras: void 0,
|
|
457
|
-
mask: void 0
|
|
314
|
+
mask: void 0,
|
|
315
|
+
visibility: void 0
|
|
316
|
+
};
|
|
317
|
+
const visibility = (column) => {
|
|
318
|
+
if (guaranteed.has(column)) return true;
|
|
319
|
+
const wheres = rules.filter((rule) => rule.columns.has(column)).map((rule) => rule.where);
|
|
320
|
+
if (wheres.length === 0) return false;
|
|
321
|
+
return wheres.length === 1 ? wheres[0] : { OR: wheres };
|
|
458
322
|
};
|
|
459
323
|
const mask = {
|
|
460
324
|
guaranteed,
|
|
@@ -474,7 +338,8 @@ function createColumnAccessResolver({ db, actions }, tableSchema) {
|
|
|
474
338
|
return {
|
|
475
339
|
columns,
|
|
476
340
|
extras,
|
|
477
|
-
mask
|
|
341
|
+
mask,
|
|
342
|
+
visibility
|
|
478
343
|
};
|
|
479
344
|
};
|
|
480
345
|
}
|
|
@@ -533,12 +398,22 @@ function createColumnMasker({ actions }, tables) {
|
|
|
533
398
|
const hidden = hiddenColumns(mask, matchedGroups(row, mask.flags), columnNames, primaryKey);
|
|
534
399
|
maskRow(row, mask, hidden);
|
|
535
400
|
state.masked.set(row, hidden.primaryKey);
|
|
401
|
+
state.hidden.set(row, hidden);
|
|
536
402
|
}
|
|
537
403
|
};
|
|
404
|
+
/**
|
|
405
|
+
* Whether masking hid any of the columns on the row. A hidden primary key
|
|
406
|
+
* counts for the columns it consists of.
|
|
407
|
+
*/
|
|
408
|
+
const columnsHidden = (abilities, row, columns, primaryKey) => {
|
|
409
|
+
const hidden = stateOf(abilities).hidden.get(row);
|
|
410
|
+
return !!hidden && columns.some((column) => hidden.cleared.includes(column) || hidden.primaryKey && primaryKey.includes(column));
|
|
411
|
+
};
|
|
538
412
|
return {
|
|
539
413
|
maskColumns,
|
|
540
414
|
maskedActions,
|
|
541
|
-
primaryKeyHidden
|
|
415
|
+
primaryKeyHidden,
|
|
416
|
+
columnsHidden
|
|
542
417
|
};
|
|
543
418
|
}
|
|
544
419
|
//#endregion
|
|
@@ -919,10 +794,11 @@ function createFilterResponder({ db, defaultLimit }, tableSchema) {
|
|
|
919
794
|
/**
|
|
920
795
|
* Packs the filters into a response object that can be applied for queries by the user
|
|
921
796
|
*/
|
|
922
|
-
return function transformToResponse(queryFilters, columnMask) {
|
|
797
|
+
return function transformToResponse(queryFilters, columnMask, columnVisibility) {
|
|
923
798
|
const ret = internalTransformer(queryFilters);
|
|
924
799
|
ret.merge = (filters) => internalTransformer(mergeFilters(ret.query.many, filters), filters.limit);
|
|
925
800
|
ret[columnMaskKey] = columnMask;
|
|
801
|
+
ret[columnVisibilityKey] = columnVisibility;
|
|
926
802
|
return ret;
|
|
927
803
|
};
|
|
928
804
|
}
|
|
@@ -1102,7 +978,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
|
1102
978
|
const mergedFilters = allQueryFilters.length === 1 ? { ...allQueryFilters[0] } : allQueryFilters.reduce((a, b) => {
|
|
1103
979
|
return mergeFilters(a, b, "OR");
|
|
1104
980
|
});
|
|
1105
|
-
const { columns, extras, mask } = resolveColumnAccess(action, allQueryFilters);
|
|
981
|
+
const { columns, extras, mask, visibility } = resolveColumnAccess(action, allQueryFilters);
|
|
1106
982
|
if (columns) mergedFilters.columns = columns;
|
|
1107
983
|
else delete mergedFilters.columns;
|
|
1108
984
|
if (extras) mergedFilters.extras = {
|
|
@@ -1110,7 +986,7 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
|
1110
986
|
...extras
|
|
1111
987
|
};
|
|
1112
988
|
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
1113
|
-
return transformToResponse(mergedFilters, mask);
|
|
989
|
+
return transformToResponse(mergedFilters, mask, visibility);
|
|
1114
990
|
};
|
|
1115
991
|
return { withContext: (userContext) => {
|
|
1116
992
|
const prepare = (action) => traceAbilityPreparation(settings, String(tableName), action, (attributes) => assembleAbilities(userContext, action, attributes));
|
|
@@ -1130,6 +1006,140 @@ const createTableAbilities = (settings, tableName, queryFilters) => {
|
|
|
1130
1006
|
} };
|
|
1131
1007
|
};
|
|
1132
1008
|
//#endregion
|
|
1009
|
+
//#region lib/abilityBuilder/inputGuard.ts
|
|
1010
|
+
/** The ts names of the columns a relation joins on, on either side. */
|
|
1011
|
+
function relationColumnKeys(db, table, columns) {
|
|
1012
|
+
const known = tableHelper({
|
|
1013
|
+
db,
|
|
1014
|
+
table
|
|
1015
|
+
}).columns;
|
|
1016
|
+
return (columns ?? []).flatMap((column) => {
|
|
1017
|
+
const raw = column?._?.column ?? column;
|
|
1018
|
+
const key = Object.entries(known).find(([name, candidate]) => candidate === raw || name === raw?.name)?.[0];
|
|
1019
|
+
return key ? [key] : [];
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
1023
|
+
/**
|
|
1024
|
+
* Column masking hides values after the query ran, but a `where` or `orderBy`
|
|
1025
|
+
* argument is applied by the database on the real values: filtering for
|
|
1026
|
+
* `{ salary: { gt: 5000 } }` reveals which rows exceed it whether or not the
|
|
1027
|
+
* column may be read. This restricts such arguments to what the abilities
|
|
1028
|
+
* expose:
|
|
1029
|
+
*
|
|
1030
|
+
* - a column only matches on rows which expose it, for all other rows the
|
|
1031
|
+
* condition is false
|
|
1032
|
+
* - sorting by a column treats it as empty on rows which do not expose it
|
|
1033
|
+
* - a relation filter requires the columns the relation joins on to be
|
|
1034
|
+
* exposed, and is restricted to the rows the caller may read of the related
|
|
1035
|
+
* table, whose masked columns follow the same rules
|
|
1036
|
+
*/
|
|
1037
|
+
function createInputGuard({ db, abilities, readActionOf }) {
|
|
1038
|
+
const visibilityOf = async (table, action) => (await abilities[table].filter(action))[columnVisibilityKey];
|
|
1039
|
+
const never = (table) => createBlockEverythingFilter(table, tableHelper({
|
|
1040
|
+
db,
|
|
1041
|
+
table
|
|
1042
|
+
})).where;
|
|
1043
|
+
/** The conditions under which the given columns are exposed. */
|
|
1044
|
+
const exposed = async (table, action, keys) => {
|
|
1045
|
+
const visibility = await visibilityOf(table, action);
|
|
1046
|
+
if (!visibility) return [];
|
|
1047
|
+
const conditions = [];
|
|
1048
|
+
for (const key of keys) {
|
|
1049
|
+
const condition = visibility(key);
|
|
1050
|
+
if (condition === true) continue;
|
|
1051
|
+
conditions.push(condition === false ? never(table) : condition);
|
|
1052
|
+
}
|
|
1053
|
+
return conditions;
|
|
1054
|
+
};
|
|
1055
|
+
/** The filter of a relation: the related rows the caller may read, exposing what it joins on. */
|
|
1056
|
+
const guardRelation = async (table, action, key, value, guarded, conditions) => {
|
|
1057
|
+
const relation = tableHelper({
|
|
1058
|
+
db,
|
|
1059
|
+
table
|
|
1060
|
+
}).relations[key];
|
|
1061
|
+
const target = relation.targetTableName;
|
|
1062
|
+
const targetAction = readActionOf(target);
|
|
1063
|
+
const targetKeys = relationColumnKeys(db, target, relation.targetColumns);
|
|
1064
|
+
conditions.push(...await exposed(table, action, relationColumnKeys(db, table, relation.sourceColumns)));
|
|
1065
|
+
const readable = (await abilities[target].filter(targetAction)).query.many.where;
|
|
1066
|
+
const related = [...readable ? [readable] : [], ...await exposed(target, targetAction, targetKeys)];
|
|
1067
|
+
const inner = isObject(value) ? await guardWhere(target, targetAction, value) : {};
|
|
1068
|
+
if (related.length === 0) guarded[key] = isObject(value) ? inner : value;
|
|
1069
|
+
else if (value === false) conditions.push({ NOT: { [key]: { AND: related } } });
|
|
1070
|
+
else guarded[key] = {
|
|
1071
|
+
...inner,
|
|
1072
|
+
AND: [...inner.AND ?? [], ...related]
|
|
1073
|
+
};
|
|
1074
|
+
};
|
|
1075
|
+
const guardLogic = (table, action, value) => Array.isArray(value) ? Promise.all(value.map((v) => guardWhere(table, action, v))) : guardWhere(table, action, value);
|
|
1076
|
+
/** A single entry of a where: a logical operator, a relation or a column. */
|
|
1077
|
+
const guardEntry = async (table, action, [key, value], guarded, conditions) => {
|
|
1078
|
+
const schema = tableHelper({
|
|
1079
|
+
db,
|
|
1080
|
+
table
|
|
1081
|
+
});
|
|
1082
|
+
if (key === "AND" || key === "OR" || key === "NOT") guarded[key] = await guardLogic(table, action, value);
|
|
1083
|
+
else if (key in schema.relations) await guardRelation(table, action, key, value, guarded, conditions);
|
|
1084
|
+
else {
|
|
1085
|
+
guarded[key] = value;
|
|
1086
|
+
if (key in schema.columns) conditions.push(...await exposed(table, action, [key]));
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
const guardWhere = async (table, action, where) => {
|
|
1090
|
+
if (!isObject(where)) return where;
|
|
1091
|
+
const guarded = {};
|
|
1092
|
+
const conditions = [];
|
|
1093
|
+
for (const entry of Object.entries(where)) await guardEntry(table, action, entry, guarded, conditions);
|
|
1094
|
+
if (conditions.length === 0) return guarded;
|
|
1095
|
+
return {
|
|
1096
|
+
...guarded,
|
|
1097
|
+
AND: [...guarded.AND ?? [], ...conditions]
|
|
1098
|
+
};
|
|
1099
|
+
};
|
|
1100
|
+
const guardOrderBy = async (table, action, orderBy) => {
|
|
1101
|
+
if (!isObject(orderBy)) return orderBy;
|
|
1102
|
+
const visibility = await visibilityOf(table, action);
|
|
1103
|
+
if (!visibility) return orderBy;
|
|
1104
|
+
const entries = Object.entries(orderBy).filter(([, direction]) => direction);
|
|
1105
|
+
if (entries.every(([key]) => visibility(key) === true)) return orderBy;
|
|
1106
|
+
const schema = tableHelper({
|
|
1107
|
+
db,
|
|
1108
|
+
table
|
|
1109
|
+
});
|
|
1110
|
+
return (aliased, { asc, desc }) => entries.map(([key, direction]) => {
|
|
1111
|
+
const condition = visibility(key);
|
|
1112
|
+
const column = aliased[key];
|
|
1113
|
+
const value = condition === true ? column : condition === false ? null : drizzle_orm.sql`case when ${(0, drizzle_orm.relationsFilterToSQL)(aliased, sanitizeFilterValue(condition), schema.relations, db._.relations)} then ${column} end`;
|
|
1114
|
+
return (direction === "asc" ? asc : desc)(value ?? drizzle_orm.sql`null`);
|
|
1115
|
+
});
|
|
1116
|
+
};
|
|
1117
|
+
return {
|
|
1118
|
+
input: async (table, action, input) => ({
|
|
1119
|
+
...input,
|
|
1120
|
+
where: await guardWhere(table, action, input.where),
|
|
1121
|
+
orderBy: await guardOrderBy(table, action, input.orderBy)
|
|
1122
|
+
}),
|
|
1123
|
+
exposed
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Restricts client supplied `where` and `orderBy` arguments to what the
|
|
1128
|
+
* abilities of the request let the caller read, see `createInputGuard`.
|
|
1129
|
+
*/
|
|
1130
|
+
function guardFilterInput(abilities, table, action, input) {
|
|
1131
|
+
const guard = abilities[guardInputKey];
|
|
1132
|
+
return guard ? guard.input(table, action, input) : Promise.resolve(input);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* The conditions a row of the related table has to meet to be reached through
|
|
1136
|
+
* a relation: the columns the relation joins on have to be exposed on it.
|
|
1137
|
+
*/
|
|
1138
|
+
async function relationExposure(abilities, table, action, columns) {
|
|
1139
|
+
const guard = abilities[guardInputKey];
|
|
1140
|
+
return guard ? guard.exposed(table, action, columns) : [];
|
|
1141
|
+
}
|
|
1142
|
+
//#endregion
|
|
1133
1143
|
//#region lib/abilityBuilder/tableBuilder.ts
|
|
1134
1144
|
/**
|
|
1135
1145
|
* Creates the registration API (allow/filter) of a single table.
|
|
@@ -1240,7 +1250,7 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
|
|
|
1240
1250
|
actions: settings.actions,
|
|
1241
1251
|
isBuilt: () => hasBeenBuilt
|
|
1242
1252
|
})]));
|
|
1243
|
-
const { maskColumns, maskedActions, primaryKeyHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
|
|
1253
|
+
const { maskColumns, maskedActions, primaryKeyHidden, columnsHidden } = createColumnMasker(settings, new Map(tableRelationNames.map((tableName) => {
|
|
1244
1254
|
const tableSchema = tableHelper({
|
|
1245
1255
|
db,
|
|
1246
1256
|
table: tableName
|
|
@@ -1263,6 +1273,8 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
|
|
|
1263
1273
|
maskedActions: (abilities, table) => maskedActions(abilities, table),
|
|
1264
1274
|
/** Whether masking in this request hid the primary key of the row. */
|
|
1265
1275
|
primaryKeyHidden: (abilities, row) => primaryKeyHidden(abilities, row),
|
|
1276
|
+
/** Whether masking in this request hid one of the columns on the row. */
|
|
1277
|
+
columnsHidden: (abilities, row, columns, primaryKey) => columnsHidden(abilities, row, columns, primaryKey),
|
|
1266
1278
|
registerReadAction(table, action) {
|
|
1267
1279
|
readActionPerTable.set(table, action);
|
|
1268
1280
|
},
|
|
@@ -1279,6 +1291,11 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
|
|
|
1279
1291
|
const state = createRequestState();
|
|
1280
1292
|
const abilities = Object.fromEntries(tableRelationNames.map((tableName) => [tableName, abilitiesPerTable[tableName].withContext(ctx)]));
|
|
1281
1293
|
attachRequestState(abilities, state);
|
|
1294
|
+
Object.defineProperty(abilities, guardInputKey, { value: createInputGuard({
|
|
1295
|
+
db,
|
|
1296
|
+
abilities,
|
|
1297
|
+
readActionOf: (table) => readActionPerTable.get(table) ?? "read"
|
|
1298
|
+
}) });
|
|
1282
1299
|
return abilities;
|
|
1283
1300
|
};
|
|
1284
1301
|
}
|
|
@@ -1286,6 +1303,152 @@ const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerC
|
|
|
1286
1303
|
};
|
|
1287
1304
|
};
|
|
1288
1305
|
//#endregion
|
|
1306
|
+
//#region lib/helpers/asserts.ts
|
|
1307
|
+
/**
|
|
1308
|
+
*
|
|
1309
|
+
* Helper function to map a drizzle findFirst query result,
|
|
1310
|
+
* which may be optional, to a correct drizzle type.
|
|
1311
|
+
*
|
|
1312
|
+
* @throws RumbleError
|
|
1313
|
+
*
|
|
1314
|
+
* @example
|
|
1315
|
+
*
|
|
1316
|
+
* ```ts
|
|
1317
|
+
* schemaBuilder.queryFields((t) => {
|
|
1318
|
+
return {
|
|
1319
|
+
findFirstUser: t.drizzleField({
|
|
1320
|
+
type: UserRef,
|
|
1321
|
+
resolve: (query, root, args, ctx, info) => {
|
|
1322
|
+
return (
|
|
1323
|
+
db.query.users
|
|
1324
|
+
.findFirst({
|
|
1325
|
+
...query,
|
|
1326
|
+
where: (await ctx.abilities.users.filter("read")).single.where,
|
|
1327
|
+
})
|
|
1328
|
+
// note that we need to manually raise an error if the value is not found
|
|
1329
|
+
.then(assertFindFirstExists)
|
|
1330
|
+
);
|
|
1331
|
+
},
|
|
1332
|
+
}),
|
|
1333
|
+
};
|
|
1334
|
+
});
|
|
1335
|
+
* ```
|
|
1336
|
+
*/
|
|
1337
|
+
const assertFindFirstExists = (value) => {
|
|
1338
|
+
if (!value) throw new RumbleErrorSafe("Value not found but required (findFirst)");
|
|
1339
|
+
return value;
|
|
1340
|
+
};
|
|
1341
|
+
/**
|
|
1342
|
+
*
|
|
1343
|
+
* Helper function to map a drizzle findFirst query result,
|
|
1344
|
+
* which may be optional, to a correct drizzle type.
|
|
1345
|
+
*
|
|
1346
|
+
* @throws RumbleError
|
|
1347
|
+
*
|
|
1348
|
+
* @example
|
|
1349
|
+
*
|
|
1350
|
+
* ```ts
|
|
1351
|
+
schemaBuilder.mutationFields((t) => {
|
|
1352
|
+
return {
|
|
1353
|
+
updateUsername: t.drizzleField({
|
|
1354
|
+
type: UserRef,
|
|
1355
|
+
args: {
|
|
1356
|
+
userId: t.arg.int({ required: true }),
|
|
1357
|
+
newName: t.arg.string({ required: true }),
|
|
1358
|
+
},
|
|
1359
|
+
resolve: (query, root, args, ctx, info) => {
|
|
1360
|
+
return db
|
|
1361
|
+
.update(schema.users)
|
|
1362
|
+
.set({
|
|
1363
|
+
name: args.newName,
|
|
1364
|
+
})
|
|
1365
|
+
.where(
|
|
1366
|
+
and(
|
|
1367
|
+
eq(schema.users.id, args.userId),
|
|
1368
|
+
(await ctx.abilities.users.filter("update")).single.where
|
|
1369
|
+
)
|
|
1370
|
+
)
|
|
1371
|
+
.returning({ id: schema.users.id, name: schema.users.name })
|
|
1372
|
+
// note that we need to manually raise an error if the value is not found
|
|
1373
|
+
.then(assertFirstEntryExists);
|
|
1374
|
+
},
|
|
1375
|
+
}),
|
|
1376
|
+
};
|
|
1377
|
+
});
|
|
1378
|
+
* ```
|
|
1379
|
+
*/
|
|
1380
|
+
const assertFirstEntryExists = (value) => {
|
|
1381
|
+
const v = value.at(0);
|
|
1382
|
+
if (!v) throw new RumbleErrorSafe("Value not found but required (firstEntry)");
|
|
1383
|
+
return v;
|
|
1384
|
+
};
|
|
1385
|
+
//#endregion
|
|
1386
|
+
//#region lib/helpers/mapNullFieldsToUndefined.ts
|
|
1387
|
+
/**
|
|
1388
|
+
* Helper to map null fields to undefined
|
|
1389
|
+
* @param obj The object to map
|
|
1390
|
+
* @returns The mapped object with all fields of 'null' transformed to 'undefined'
|
|
1391
|
+
*
|
|
1392
|
+
* This becomes useful for update mutations where you do not want to pass null (unset value in db)
|
|
1393
|
+
* but undefined (do not touch value in db) in case of a value not beeing set in the args of said mutation
|
|
1394
|
+
* @example
|
|
1395
|
+
* ```ts
|
|
1396
|
+
* updateUser: t.drizzleField({
|
|
1397
|
+
type: User,
|
|
1398
|
+
args: {
|
|
1399
|
+
id: t.arg.string({ required: true }),
|
|
1400
|
+
email: t.arg.string(),
|
|
1401
|
+
lastName: t.arg.string(),
|
|
1402
|
+
firstName: t.arg.string(),
|
|
1403
|
+
},
|
|
1404
|
+
resolve: async (query, root, args, ctx, info) => {
|
|
1405
|
+
const mappedArgs = mapNullFieldsToUndefined(args);
|
|
1406
|
+
const user = await db.transaction(async (tx) => {
|
|
1407
|
+
const user = await tx
|
|
1408
|
+
.update(schema.user)
|
|
1409
|
+
.set({
|
|
1410
|
+
|
|
1411
|
+
// email: args.email ?? undefined,
|
|
1412
|
+
// lastName: args.lastName ?? undefined,
|
|
1413
|
+
// firstName: args.firstName ?? undefined,
|
|
1414
|
+
|
|
1415
|
+
// becomes this
|
|
1416
|
+
|
|
1417
|
+
email: mappedArgs.email,
|
|
1418
|
+
lastName: mappedArgs.lastName,
|
|
1419
|
+
firstName: mappedArgs.firstName,
|
|
1420
|
+
})
|
|
1421
|
+
.returning()
|
|
1422
|
+
.then(assertFirstEntryExists);
|
|
1423
|
+
return user;
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
pubsub.updated(user.id);
|
|
1427
|
+
|
|
1428
|
+
return db.query.user
|
|
1429
|
+
.findFirst(
|
|
1430
|
+
query(
|
|
1431
|
+
(await ctx.abilities.user.filter('read')).merge(
|
|
1432
|
+
{
|
|
1433
|
+
where: { id: user.id },
|
|
1434
|
+
}
|
|
1435
|
+
1).query.single,
|
|
1436
|
+
),
|
|
1437
|
+
)
|
|
1438
|
+
.then(assertFindFirstExists);
|
|
1439
|
+
},
|
|
1440
|
+
}),
|
|
1441
|
+
*
|
|
1442
|
+
*
|
|
1443
|
+
* ```
|
|
1444
|
+
*/
|
|
1445
|
+
function mapNullFieldsToUndefined(obj) {
|
|
1446
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value === null ? void 0 : value]));
|
|
1447
|
+
}
|
|
1448
|
+
//#endregion
|
|
1449
|
+
//#region package.json
|
|
1450
|
+
var version = "0.26.2";
|
|
1451
|
+
//#endregion
|
|
1289
1452
|
//#region lib/args/orderArg.ts
|
|
1290
1453
|
const makeDefaultName$1 = (dbName) => `${(0, es_toolkit.capitalize)((0, drizzle_orm_casing.toCamelCase)(dbName.toString()))}OrderInputArgument`;
|
|
1291
1454
|
const createOrderArgImplementer = ({ db, schemaBuilder }) => {
|
|
@@ -1712,6 +1875,8 @@ const createCountQueryImplementer = ({ db, schemaBuilder, whereArgImplementer, m
|
|
|
1712
1875
|
resolve: async (_root, args, ctx, _info) => {
|
|
1713
1876
|
if (isAllowed && !await isAllowed(ctx)) throw new RumbleErrorSafe("Not allowed to perform this action");
|
|
1714
1877
|
deepSetProto(args);
|
|
1878
|
+
const guarded = await guardFilterInput(ctx.abilities, String(table), listAction, { where: args.where });
|
|
1879
|
+
if (args.where) args.where = guarded.where;
|
|
1715
1880
|
return db.select({ count: (0, drizzle_orm.count)() }).from(tableSchema.table).where((await ctx.abilities[table].filter(listAction)).merge(mapNullFieldsToUndefined(args)).sql.where).then(assertFirstEntryExists).then((r) => r.count);
|
|
1716
1881
|
}
|
|
1717
1882
|
}) };
|
|
@@ -2109,9 +2274,12 @@ function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
|
|
|
2109
2274
|
args.extras = { search_distance: (table) => drizzle_orm.sql`${drizzle_orm.sql.join(columnsToSearch.map(([key]) => {
|
|
2110
2275
|
return drizzle_orm.sql`COALESCE((${table[key]}::TEXT <-> ${searchParam}), 1)`;
|
|
2111
2276
|
}), drizzle_orm.sql.raw(" + "))}` };
|
|
2112
|
-
const originalOrderBy = (0, es_toolkit.cloneDeep)(args.orderBy);
|
|
2277
|
+
const originalOrderBy = typeof args.orderBy === "function" ? args.orderBy : (0, es_toolkit.cloneDeep)(args.orderBy);
|
|
2113
2278
|
args.orderBy = (table) => {
|
|
2114
|
-
const argsOrderBySQL = drizzle_orm.sql.join(
|
|
2279
|
+
const argsOrderBySQL = drizzle_orm.sql.join(typeof originalOrderBy === "function" ? originalOrderBy(table, {
|
|
2280
|
+
asc: drizzle_orm.asc,
|
|
2281
|
+
desc: drizzle_orm.desc
|
|
2282
|
+
}) : Object.entries(originalOrderBy ?? {}).map(([key, value]) => {
|
|
2115
2283
|
if (value === "asc") return drizzle_orm.sql`${table[key]} ASC`;
|
|
2116
2284
|
else if (value === "desc") return drizzle_orm.sql`${table[key]} DESC`;
|
|
2117
2285
|
else throw new Error(`Invalid value ${value} for orderBy`);
|
|
@@ -2195,6 +2363,13 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
2195
2363
|
fields: Object.keys(tableSchema.primaryKey),
|
|
2196
2364
|
isHidden: (context, row) => abilityBuilder._.primaryKeyHidden(context.abilities, row)
|
|
2197
2365
|
},
|
|
2366
|
+
maskRelations: {
|
|
2367
|
+
fields: Object.fromEntries(Object.entries(tableSchema.relations ?? {}).map(([key, relation]) => [key, {
|
|
2368
|
+
many: !(0, drizzle_orm.is)(relation, drizzle_orm.One),
|
|
2369
|
+
sourceColumns: relationColumnKeys(db, tableSchema.tsName, relation.sourceColumns)
|
|
2370
|
+
}])),
|
|
2371
|
+
isHidden: (context, row, columns) => abilityBuilder._.columnsHidden(context.abilities, row, columns, Object.keys(tableSchema.primaryKey))
|
|
2372
|
+
},
|
|
2198
2373
|
fields: (t) => {
|
|
2199
2374
|
const columns = tableSchema.columns;
|
|
2200
2375
|
const configMap = /* @__PURE__ */ new Map();
|
|
@@ -2261,7 +2436,7 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
2261
2436
|
let filterSpecifier = "many";
|
|
2262
2437
|
if ((0, drizzle_orm.is)(value, drizzle_orm.One)) {
|
|
2263
2438
|
isMany = false;
|
|
2264
|
-
nullable = value.optional || !value.sourceColumns.every((column) => column.notNull);
|
|
2439
|
+
nullable = value.optional || !value.sourceColumns.every((column) => (column._?.column ?? column).notNull);
|
|
2265
2440
|
filterSpecifier = "single";
|
|
2266
2441
|
}
|
|
2267
2442
|
const subscribe = (subscriptions, _element) => {
|
|
@@ -2304,14 +2479,21 @@ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schem
|
|
|
2304
2479
|
description: `Get the ${pluralize.default.plural(relationSchema.tsName)} related to this ${pluralize.default.singular(tableSchema.tsName)}`,
|
|
2305
2480
|
query: async (args, ctx) => {
|
|
2306
2481
|
args = JSON.parse(JSON.stringify(args));
|
|
2482
|
+
const guarded = await guardFilterInput(ctx.abilities, relationSchema.tsName, readAction, {
|
|
2483
|
+
where: args.where,
|
|
2484
|
+
orderBy: args.orderBy
|
|
2485
|
+
});
|
|
2486
|
+
if (args.where) args.where = guarded.where;
|
|
2487
|
+
if (args.orderBy) args.orderBy = guarded.orderBy;
|
|
2307
2488
|
if (isMany) adjustQueryArgsForSearch({
|
|
2308
2489
|
search,
|
|
2309
2490
|
args,
|
|
2310
2491
|
tableSchema: relationSchema,
|
|
2311
2492
|
abilities: await ctx.abilities[relationSchema.tsName].filter(readAction)
|
|
2312
2493
|
});
|
|
2494
|
+
const joined = await relationExposure(ctx.abilities, relationSchema.tsName, readAction, relationColumnKeys(db, relationSchema.tsName, value.targetColumns));
|
|
2313
2495
|
const filter = (await ctx.abilities[relationSchema.tsName].filter(readAction)).merge({
|
|
2314
|
-
where: args.where,
|
|
2496
|
+
where: joined.length ? { AND: [...args.where ? [args.where] : [], ...joined] } : args.where,
|
|
2315
2497
|
limit: args.limit,
|
|
2316
2498
|
extras: args.extras
|
|
2317
2499
|
}).query[filterSpecifier];
|
|
@@ -2473,6 +2655,12 @@ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: logge
|
|
|
2473
2655
|
args: manyArgs,
|
|
2474
2656
|
resolve: async (query, _root, args, ctx, _info) => {
|
|
2475
2657
|
deepSetProto(args);
|
|
2658
|
+
const guarded = await guardFilterInput(ctx.abilities, String(table), listAction, {
|
|
2659
|
+
where: args.where,
|
|
2660
|
+
orderBy: args.orderBy
|
|
2661
|
+
});
|
|
2662
|
+
if (args.where) args.where = guarded.where;
|
|
2663
|
+
if (args.orderBy) args.orderBy = guarded.orderBy;
|
|
2476
2664
|
adjustQueryArgsForSearch({
|
|
2477
2665
|
search,
|
|
2478
2666
|
args,
|
|
@@ -2800,12 +2988,19 @@ function registerPluginOnce(name, plugin) {
|
|
|
2800
2988
|
const maskColumnsKey = "maskColumns";
|
|
2801
2989
|
const maskColumnsActionKey = "maskColumnsAction";
|
|
2802
2990
|
const maskPrimaryKeysKey = "maskPrimaryKeys";
|
|
2991
|
+
const maskRelationsKey = "maskRelations";
|
|
2803
2992
|
const columnMaskPluginName = "ColumnMaskPlugin";
|
|
2804
2993
|
//#endregion
|
|
2805
2994
|
//#region lib/columnMaskPlugin/columnMaskPlugin.ts
|
|
2806
2995
|
var ColumnMaskPlugin = class extends _pothos_core.BasePlugin {
|
|
2807
2996
|
wrapResolve(resolver, fieldConfig) {
|
|
2808
|
-
return this.maskResult(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig);
|
|
2997
|
+
return this.maskResult(this.maskRelation(this.maskPrimaryKey(resolver, fieldConfig), fieldConfig), fieldConfig);
|
|
2998
|
+
}
|
|
2999
|
+
maskRelation(resolver, fieldConfig) {
|
|
3000
|
+
const relations = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskRelationsKey];
|
|
3001
|
+
const relation = relations?.fields[fieldConfig.name];
|
|
3002
|
+
if (!relations || !relation) return resolver;
|
|
3003
|
+
return (parent, args, context, info) => relations.isHidden(context, parent, relation.sourceColumns) ? relation.many ? [] : null : resolver(parent, args, context, info);
|
|
2809
3004
|
}
|
|
2810
3005
|
maskPrimaryKey(resolver, fieldConfig) {
|
|
2811
3006
|
const primaryKeys = this.buildCache.getTypeConfig(fieldConfig.parentType).pothosOptions[maskPrimaryKeysKey];
|
|
@@ -3625,6 +3820,7 @@ exports.RumbleError = RumbleError;
|
|
|
3625
3820
|
exports.RumbleErrorSafe = RumbleErrorSafe;
|
|
3626
3821
|
exports.assertFindFirstExists = assertFindFirstExists;
|
|
3627
3822
|
exports.assertFirstEntryExists = assertFirstEntryExists;
|
|
3823
|
+
exports.guardFilterInput = guardFilterInput;
|
|
3628
3824
|
exports.mapNullFieldsToUndefined = mapNullFieldsToUndefined;
|
|
3629
3825
|
exports.normalizeEmailAddress = normalizeEmailAddress;
|
|
3630
3826
|
exports.normalizePersonName = normalizePersonName;
|