@simtlix/simfinity-js 2.4.4 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +23 -0
  2. package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
  3. package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
  4. package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
  5. package/.claude/worktrees/agitated-kepler/README.md +3941 -0
  6. package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
  7. package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
  8. package/.claude/worktrees/agitated-kepler/package.json +41 -0
  9. package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
  10. package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
  11. package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
  12. package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
  13. package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
  14. package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
  15. package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
  16. package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
  17. package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
  18. package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
  19. package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
  20. package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
  21. package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
  22. package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
  23. package/.cursor/rules/simfinity-core-functions.mdc +3 -1
  24. package/README.md +202 -0
  25. package/git-report.js +224 -0
  26. package/package.json +1 -1
  27. package/src/index.js +237 -23
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Simfinity GraphQL Authorization
3
+ *
4
+ * Production-grade centralized GraphQL authorization supporting:
5
+ * - RBAC / ABAC
6
+ * - Function-based rules
7
+ * - Declarative policy expressions (JSON AST)
8
+ * - Wildcard "*" permissions
9
+ * - Default allow/deny policies
10
+ *
11
+ * @example
12
+ * import { auth } from '@simtlix/simfinity-js';
13
+ * import { createYoga } from 'graphql-yoga';
14
+ *
15
+ * const { createAuthPlugin, requireAuth, requireRole } = auth;
16
+ *
17
+ * const permissions = {
18
+ * Query: {
19
+ * series: requireAuth(),
20
+ * seasons: requireAuth(),
21
+ * },
22
+ * Mutation: {
23
+ * deleteserie: requireRole('admin'),
24
+ * deletestar: requireRole('admin'),
25
+ * },
26
+ * serie: {
27
+ * '*': requireAuth(),
28
+ * }
29
+ * };
30
+ *
31
+ * const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'ALLOW' });
32
+ * const yoga = createYoga({ schema, plugins: [authPlugin] });
33
+ */
34
+
35
+ import { GraphQLObjectType, defaultFieldResolver } from 'graphql';
36
+ import { UnauthenticatedError, ForbiddenError, createAuthError } from './errors.js';
37
+ import { isPolicyExpression, createRuleFromExpression, evaluateExpression } from './expressions.js';
38
+ import {
39
+ resolvePath,
40
+ requireAuth,
41
+ requireRole,
42
+ requirePermission,
43
+ composeRules,
44
+ anyRule,
45
+ isOwner,
46
+ createRule,
47
+ allow,
48
+ deny,
49
+ } from './rules.js';
50
+
51
+ // Re-export errors
52
+ export { UnauthenticatedError, ForbiddenError, createAuthError } from './errors.js';
53
+
54
+ // Re-export expression utilities
55
+ export { evaluateExpression, isPolicyExpression, createRuleFromExpression } from './expressions.js';
56
+
57
+ // Re-export rule helpers and utilities
58
+ export {
59
+ resolvePath,
60
+ requireAuth,
61
+ requireRole,
62
+ requirePermission,
63
+ composeRules,
64
+ anyRule,
65
+ isOwner,
66
+ createRule,
67
+ allow,
68
+ deny,
69
+ } from './rules.js';
70
+
71
+ /**
72
+ * @typedef {'ALLOW' | 'DENY'} DefaultPolicy
73
+ */
74
+
75
+ /**
76
+ * @typedef {Object} AuthMiddlewareOptions
77
+ * @property {DefaultPolicy} [defaultPolicy='DENY'] - Default policy when no rule matches
78
+ * @property {boolean} [debug=false] - Enable debug logging
79
+ */
80
+
81
+ /**
82
+ * @typedef {Function} RuleFunction
83
+ * @param {*} parent - Parent resolver result
84
+ * @param {Object} args - GraphQL arguments
85
+ * @param {Object} ctx - GraphQL context
86
+ * @param {Object} info - GraphQL resolve info
87
+ * @returns {boolean|void|Promise<boolean|void>} - true/void to allow, false to deny
88
+ */
89
+
90
+ /**
91
+ * @typedef {Object|RuleFunction|Array<RuleFunction>} Rule
92
+ */
93
+
94
+ /**
95
+ * @typedef {Object.<string, Rule>} TypePermissions
96
+ */
97
+
98
+ /**
99
+ * @typedef {Object.<string, TypePermissions>} PermissionSchema
100
+ */
101
+
102
+ /**
103
+ * Normalizes a rule to always be an array of functions
104
+ * @param {Rule} rule - The rule to normalize
105
+ * @returns {Function[]} Array of rule functions
106
+ */
107
+ const normalizeRule = (rule) => {
108
+ if (typeof rule === 'function') {
109
+ return [rule];
110
+ }
111
+
112
+ if (Array.isArray(rule)) {
113
+ return rule.flatMap(r => normalizeRule(r));
114
+ }
115
+
116
+ if (isPolicyExpression(rule)) {
117
+ return [createRuleFromExpression(rule)];
118
+ }
119
+
120
+ // Unknown rule type - return empty (will use default policy)
121
+ return [];
122
+ };
123
+
124
+ /**
125
+ * Gets the rule for a specific field, with wildcard fallback
126
+ * @param {PermissionSchema} permissions - The permission schema
127
+ * @param {string} typeName - The GraphQL type name
128
+ * @param {string} fieldName - The field name
129
+ * @returns {Function[]|null} Array of rule functions or null if no rule found
130
+ */
131
+ const getFieldRules = (permissions, typeName, fieldName) => {
132
+ const typePerms = permissions[typeName];
133
+
134
+ if (!typePerms) {
135
+ return null;
136
+ }
137
+
138
+ // Check for exact field rule first
139
+ if (fieldName in typePerms) {
140
+ return normalizeRule(typePerms[fieldName]);
141
+ }
142
+
143
+ // Fallback to wildcard
144
+ if ('*' in typePerms) {
145
+ return normalizeRule(typePerms['*']);
146
+ }
147
+
148
+ return null;
149
+ };
150
+
151
+ /**
152
+ * Executes a single rule
153
+ * @param {Function} rule - The rule function to execute
154
+ * @param {*} parent - Parent resolver result
155
+ * @param {Object} args - GraphQL arguments
156
+ * @param {Object} ctx - GraphQL context
157
+ * @param {Object} info - GraphQL resolve info
158
+ * @returns {Promise<boolean>} True if allowed, false if denied
159
+ */
160
+ const executeRule = async (rule, parent, args, ctx, info) => {
161
+ const result = await rule(parent, args, ctx, info);
162
+
163
+ // void/undefined/true means allow
164
+ if (result === undefined || result === true) {
165
+ return true;
166
+ }
167
+
168
+ // false means deny
169
+ return false;
170
+ };
171
+
172
+ /**
173
+ * Creates a graphql-middleware compatible authorization middleware.
174
+ *
175
+ * @deprecated Use {@link createAuthPlugin} instead. `applyMiddleware` from graphql-middleware
176
+ * can cause duplicate-type errors when the schema contains custom introspection extensions.
177
+ *
178
+ * @param {PermissionSchema} permissions - The permission schema object
179
+ * @param {AuthMiddlewareOptions} [options={}] - Middleware options
180
+ * @returns {Function} A graphql-middleware compatible middleware function
181
+ */
182
+ export const createAuthMiddleware = (permissions, options = {}) => {
183
+ const {
184
+ defaultPolicy = 'DENY',
185
+ debug = false,
186
+ } = options;
187
+
188
+ const log = debug ? console.log.bind(console, '[auth]') : () => {};
189
+
190
+ /**
191
+ * The middleware generator function
192
+ * Returns a middleware object keyed by type name, each containing field resolvers
193
+ */
194
+ return async (resolve, parent, args, ctx, info) => {
195
+ const typeName = info.parentType.name;
196
+ const fieldName = info.fieldName;
197
+
198
+ log(`Checking ${typeName}.${fieldName}`);
199
+
200
+ // Get rules for this field
201
+ const rules = getFieldRules(permissions, typeName, fieldName);
202
+
203
+ // If no rules found, apply default policy
204
+ if (rules === null || rules.length === 0) {
205
+ log(`No rules for ${typeName}.${fieldName}, applying default policy: ${defaultPolicy}`);
206
+
207
+ if (defaultPolicy === 'DENY') {
208
+ throw new ForbiddenError(`Access denied to ${typeName}.${fieldName}`);
209
+ }
210
+
211
+ // ALLOW - proceed to resolver
212
+ return resolve(parent, args, ctx, info);
213
+ }
214
+
215
+ // Execute all rules (AND logic - all must pass)
216
+ for (const rule of rules) {
217
+ log(`Executing rule for ${typeName}.${fieldName}`);
218
+
219
+ const allowed = await executeRule(rule, parent, args, ctx, info);
220
+
221
+ if (!allowed) {
222
+ log(`Rule denied access to ${typeName}.${fieldName}`);
223
+ throw new ForbiddenError(`Access denied to ${typeName}.${fieldName}`);
224
+ }
225
+ }
226
+
227
+ log(`Access granted to ${typeName}.${fieldName}`);
228
+
229
+ // All rules passed - proceed to resolver
230
+ return resolve(parent, args, ctx, info);
231
+ };
232
+ };
233
+
234
+ /**
235
+ * Creates a field-level middleware object from a permission schema.
236
+ * This can be used with graphql-middleware's applyMiddleware.
237
+ *
238
+ * @deprecated Use {@link createAuthPlugin} instead. `applyMiddleware` from graphql-middleware
239
+ * can cause duplicate-type errors when the schema contains custom introspection extensions.
240
+ *
241
+ * @param {PermissionSchema} permissions - The permission schema
242
+ * @param {AuthMiddlewareOptions} [options={}] - Middleware options
243
+ * @returns {Object} Field middleware object compatible with graphql-middleware
244
+ */
245
+ export const createFieldMiddleware = (permissions, options = {}) => {
246
+ const middleware = createAuthMiddleware(permissions, options);
247
+ const fieldMiddleware = {};
248
+
249
+ for (const typeName of Object.keys(permissions)) {
250
+ fieldMiddleware[typeName] = {};
251
+
252
+ const typePerms = permissions[typeName];
253
+ for (const fieldName of Object.keys(typePerms)) {
254
+ if (fieldName === '*') {
255
+ // Wildcard rules are handled by the middleware internally
256
+ continue;
257
+ }
258
+ fieldMiddleware[typeName][fieldName] = middleware;
259
+ }
260
+ }
261
+
262
+ return fieldMiddleware;
263
+ };
264
+
265
+ /**
266
+ * Creates an Envelop-compatible authorization plugin that wraps schema resolvers in-place.
267
+ *
268
+ * Unlike {@link createAuthMiddleware} (which requires graphql-middleware's `applyMiddleware`
269
+ * and rebuilds the schema), this plugin mutates resolvers directly on the existing schema,
270
+ * avoiding schema reconstruction and the duplicate-type errors it can cause.
271
+ *
272
+ * @param {PermissionSchema} permissions - The permission schema object
273
+ * @param {AuthMiddlewareOptions} [options={}] - Plugin options
274
+ * @returns {Object} An Envelop plugin with an `onSchemaChange` hook
275
+ *
276
+ * @example
277
+ * import { auth } from '@simtlix/simfinity-js';
278
+ * import { createYoga } from 'graphql-yoga';
279
+ *
280
+ * const permissions = {
281
+ * Query: {
282
+ * users: requireAuth(),
283
+ * adminDashboard: requireRole('ADMIN')
284
+ * },
285
+ * User: {
286
+ * '*': requireAuth(),
287
+ * email: requireRole('ADMIN')
288
+ * }
289
+ * };
290
+ *
291
+ * const authPlugin = auth.createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
292
+ * const yoga = createYoga({ schema, plugins: [authPlugin] });
293
+ */
294
+ export const createAuthPlugin = (permissions, options = {}) => {
295
+ const {
296
+ defaultPolicy = 'DENY',
297
+ debug = false,
298
+ } = options;
299
+
300
+ const log = debug ? console.log.bind(console, '[auth]') : () => {};
301
+ const processedSchemas = new WeakSet();
302
+
303
+ const wrapSchemaResolvers = (schema) => {
304
+ if (processedSchemas.has(schema)) return;
305
+
306
+ const typeMap = schema.getTypeMap();
307
+
308
+ for (const [typeName, type] of Object.entries(typeMap)) {
309
+ if (!(type instanceof GraphQLObjectType) || typeName.startsWith('__')) continue;
310
+
311
+ const fields = type.getFields();
312
+
313
+ for (const [fieldName, field] of Object.entries(fields)) {
314
+ const rules = getFieldRules(permissions, typeName, fieldName);
315
+ const originalResolve = field.resolve || defaultFieldResolver;
316
+
317
+ field.resolve = async (parent, args, ctx, info) => {
318
+ log(`Checking ${typeName}.${fieldName}`);
319
+
320
+ if (rules === null || rules.length === 0) {
321
+ log(`No rules for ${typeName}.${fieldName}, applying default policy: ${defaultPolicy}`);
322
+
323
+ if (defaultPolicy === 'DENY') {
324
+ throw new ForbiddenError(`Access denied to ${typeName}.${fieldName}`);
325
+ }
326
+
327
+ return originalResolve(parent, args, ctx, info);
328
+ }
329
+
330
+ for (const rule of rules) {
331
+ log(`Executing rule for ${typeName}.${fieldName}`);
332
+
333
+ const allowed = await executeRule(rule, parent, args, ctx, info);
334
+
335
+ if (!allowed) {
336
+ log(`Rule denied access to ${typeName}.${fieldName}`);
337
+ throw new ForbiddenError(`Access denied to ${typeName}.${fieldName}`);
338
+ }
339
+ }
340
+
341
+ log(`Access granted to ${typeName}.${fieldName}`);
342
+
343
+ return originalResolve(parent, args, ctx, info);
344
+ };
345
+ }
346
+ }
347
+
348
+ processedSchemas.add(schema);
349
+ };
350
+
351
+ return {
352
+ onSchemaChange({ schema }) {
353
+ wrapSchemaResolvers(schema);
354
+ },
355
+ };
356
+ };
357
+
358
+ // Default export with all auth utilities
359
+ const auth = {
360
+ // Main factories
361
+ createAuthPlugin,
362
+ createAuthMiddleware,
363
+ createFieldMiddleware,
364
+
365
+ // Utilities
366
+ resolvePath,
367
+
368
+ // Rule helpers
369
+ requireAuth,
370
+ requireRole,
371
+ requirePermission,
372
+ composeRules,
373
+ anyRule,
374
+ isOwner,
375
+ createRule,
376
+ allow,
377
+ deny,
378
+
379
+ // Expression utilities
380
+ evaluateExpression,
381
+ isPolicyExpression,
382
+ createRuleFromExpression,
383
+
384
+ // Errors
385
+ UnauthenticatedError,
386
+ ForbiddenError,
387
+ createAuthError,
388
+ };
389
+
390
+ export default auth;
391
+
@@ -0,0 +1,274 @@
1
+ import { UnauthenticatedError, ForbiddenError } from './errors.js';
2
+
3
+ /**
4
+ * Resolves a value from an object using a dotted path string or function.
5
+ * @param {Object} obj - The object to resolve from
6
+ * @param {string|Function} pathOrFn - Dotted path (e.g., 'user.profile.id') or function to extract value
7
+ * @returns {*} The resolved value or undefined if not found
8
+ * @example
9
+ * resolvePath({ user: { id: '123' } }, 'user.id') // returns '123'
10
+ * resolvePath({ user: { id: '123' } }, (obj) => obj.user.id) // returns '123'
11
+ */
12
+ export const resolvePath = (obj, pathOrFn) => {
13
+ if (typeof pathOrFn === 'function') {
14
+ return pathOrFn(obj);
15
+ }
16
+ if (typeof pathOrFn === 'string') {
17
+ const parts = pathOrFn.split('.');
18
+ let value = obj;
19
+ for (const part of parts) {
20
+ if (value == null) return undefined;
21
+ value = value[part];
22
+ }
23
+ return value;
24
+ }
25
+ return undefined;
26
+ };
27
+
28
+ /**
29
+ * Rule that requires the user to be authenticated
30
+ * Checks for user existence at the specified path in context
31
+ * @param {string|Function} [userPath='user'] - Path to user in context (e.g., 'user', 'auth.user', 'session.user')
32
+ * @returns {Function} Rule function (parent, args, ctx, info) => boolean | throws
33
+ * @example
34
+ * requireAuth() // checks ctx.user
35
+ * requireAuth('auth.user') // checks ctx.auth.user
36
+ * requireAuth('session.currentUser') // checks ctx.session.currentUser
37
+ */
38
+ export const requireAuth = (userPath = 'user') => {
39
+ return (_parent, _args, ctx) => {
40
+ if (!ctx) {
41
+ throw new UnauthenticatedError('You must be logged in to access this resource');
42
+ }
43
+ const user = resolvePath(ctx, userPath);
44
+ if (!user) {
45
+ throw new UnauthenticatedError('You must be logged in to access this resource');
46
+ }
47
+ return true;
48
+ };
49
+ };
50
+
51
+ /**
52
+ * Rule that requires the user to have a specific role
53
+ * @param {string|string[]} role - Required role or array of roles (any match)
54
+ * @param {Object} [options] - Configuration options
55
+ * @param {string|Function} [options.userPath='user'] - Path to user in context
56
+ * @param {string|Function} [options.rolePath='role'] - Path to role field in user object
57
+ * @returns {Function} Rule function (parent, args, ctx, info) => boolean | throws
58
+ * @example
59
+ * requireRole('ADMIN')
60
+ * requireRole(['ADMIN', 'EDITOR'])
61
+ * requireRole('ADMIN', { userPath: 'auth.user', rolePath: 'roles.primary' })
62
+ */
63
+ export const requireRole = (role, options = {}) => {
64
+ const roles = Array.isArray(role) ? role : [role];
65
+ const { userPath = 'user', rolePath = 'role' } = options;
66
+
67
+ return (_parent, _args, ctx) => {
68
+ if (!ctx) {
69
+ throw new UnauthenticatedError('You must be logged in to access this resource');
70
+ }
71
+ const user = resolvePath(ctx, userPath);
72
+ if (!user) {
73
+ throw new UnauthenticatedError('You must be logged in to access this resource');
74
+ }
75
+
76
+ const userRole = resolvePath(user, rolePath);
77
+
78
+ if (!roles.includes(userRole)) {
79
+ throw new ForbiddenError(`Requires role: ${roles.join(' or ')}`);
80
+ }
81
+
82
+ return true;
83
+ };
84
+ };
85
+
86
+ /**
87
+ * Rule that requires the user to have a specific permission
88
+ * @param {string|string[]} permission - Required permission(s) (all must match if array)
89
+ * @param {Object} [options] - Configuration options
90
+ * @param {string|Function} [options.userPath='user'] - Path to user in context
91
+ * @param {string|Function} [options.permissionsPath='permissions'] - Path to permissions array in user object
92
+ * @returns {Function} Rule function (parent, args, ctx, info) => boolean | throws
93
+ * @example
94
+ * requirePermission('posts:read')
95
+ * requirePermission(['posts:read', 'posts:write'])
96
+ * requirePermission('posts:read', { userPath: 'auth.user', permissionsPath: 'grants' })
97
+ */
98
+ export const requirePermission = (permission, options = {}) => {
99
+ const requiredPermissions = Array.isArray(permission) ? permission : [permission];
100
+ const { userPath = 'user', permissionsPath = 'permissions' } = options;
101
+
102
+ return (_parent, _args, ctx) => {
103
+ if (!ctx) {
104
+ throw new UnauthenticatedError('You must be logged in to access this resource');
105
+ }
106
+ const user = resolvePath(ctx, userPath);
107
+ if (!user) {
108
+ throw new UnauthenticatedError('You must be logged in to access this resource');
109
+ }
110
+
111
+ const userPermissions = resolvePath(user, permissionsPath) || [];
112
+
113
+ // Check if user has wildcard permission
114
+ if (userPermissions.includes('*')) {
115
+ return true;
116
+ }
117
+
118
+ // All required permissions must be present
119
+ for (const perm of requiredPermissions) {
120
+ if (!userPermissions.includes(perm)) {
121
+ throw new ForbiddenError(`Missing permission: ${perm}`);
122
+ }
123
+ }
124
+
125
+ return true;
126
+ };
127
+ };
128
+
129
+ /**
130
+ * Composes multiple rules - all must pass (logical AND)
131
+ * @param {...Function} rules - Rule functions to compose
132
+ * @returns {Function} Composed rule function
133
+ */
134
+ export const composeRules = (...rules) => {
135
+ return async (parent, args, ctx, info) => {
136
+ for (const rule of rules) {
137
+ const result = await rule(parent, args, ctx, info);
138
+ // If rule returns false, deny access
139
+ if (result === false) {
140
+ return false;
141
+ }
142
+ // If rule throws, it will propagate
143
+ }
144
+ return true;
145
+ };
146
+ };
147
+
148
+ /**
149
+ * Creates a rule that allows access if ANY of the provided rules pass (logical OR)
150
+ * @param {...Function} rules - Rule functions to check
151
+ * @returns {Function} Combined rule function
152
+ */
153
+ export const anyRule = (...rules) => {
154
+ return async (parent, args, ctx, info) => {
155
+ let lastError = null;
156
+
157
+ for (const rule of rules) {
158
+ try {
159
+ const result = await rule(parent, args, ctx, info);
160
+ if (result !== false) {
161
+ return true; // At least one rule passed
162
+ }
163
+ } catch (error) {
164
+ lastError = error;
165
+ // Continue to next rule
166
+ }
167
+ }
168
+
169
+ // No rule passed - throw the last error or return false
170
+ if (lastError) {
171
+ throw lastError;
172
+ }
173
+ return false;
174
+ };
175
+ };
176
+
177
+ /**
178
+ * Creates a rule that checks if the authenticated user owns the resource
179
+ * @param {string|Function} [ownerField='userId'] - Path to owner ID in parent, or function to extract it
180
+ * @param {string|Function} [userIdField='id'] - Path to user ID in user object, or function to extract it
181
+ * @param {Object} [options] - Configuration options
182
+ * @param {string|Function} [options.userPath='user'] - Path to user in context
183
+ * @returns {Function} Rule function
184
+ * @example
185
+ * isOwner() // compares parent.userId with ctx.user.id
186
+ * isOwner('authorId') // compares parent.authorId with ctx.user.id
187
+ * isOwner('author.id', 'profile.id') // compares parent.author.id with ctx.user.profile.id
188
+ * isOwner('authorId', 'id', { userPath: 'auth.user' }) // uses ctx.auth.user instead of ctx.user
189
+ */
190
+ export const isOwner = (ownerField = 'userId', userIdField = 'id', options = {}) => {
191
+ const { userPath = 'user' } = options;
192
+
193
+ return (parent, _args, ctx) => {
194
+ if (!ctx) {
195
+ throw new UnauthenticatedError('You must be logged in to access this resource');
196
+ }
197
+ const user = resolvePath(ctx, userPath);
198
+ if (!user) {
199
+ throw new UnauthenticatedError('You must be logged in to access this resource');
200
+ }
201
+
202
+ // Get ownerId from parent (using path or function)
203
+ const ownerId = resolvePath(parent, ownerField);
204
+
205
+ // Get userId from user object (using path or function)
206
+ const userId = resolvePath(user, userIdField);
207
+
208
+ if (ownerId === undefined || userId === undefined) {
209
+ return false;
210
+ }
211
+
212
+ return String(ownerId) === String(userId);
213
+ };
214
+ };
215
+
216
+ /**
217
+ * Creates a custom rule from a predicate function
218
+ * @param {Function} predicate - Function (parent, args, ctx, info) => boolean | Promise<boolean>
219
+ * @param {string} errorMessage - Error message if rule fails
220
+ * @param {string} errorCode - Error code (FORBIDDEN or UNAUTHENTICATED)
221
+ * @returns {Function} Rule function
222
+ */
223
+ export const createRule = (predicate, errorMessage = 'Access denied', errorCode = 'FORBIDDEN') => {
224
+ return async (parent, args, ctx, info) => {
225
+ const result = await predicate(parent, args, ctx, info);
226
+
227
+ if (result === false) {
228
+ if (errorCode === 'UNAUTHENTICATED') {
229
+ throw new UnauthenticatedError(errorMessage);
230
+ }
231
+ throw new ForbiddenError(errorMessage);
232
+ }
233
+
234
+ return true;
235
+ };
236
+ };
237
+
238
+ /**
239
+ * Rule that always allows access (useful for public fields)
240
+ * @returns {Function} Rule function that always returns true
241
+ */
242
+ export const allow = () => {
243
+ return () => true;
244
+ };
245
+
246
+ /**
247
+ * Rule that always denies access
248
+ * @param {string} message - Optional denial message
249
+ * @returns {Function} Rule function that always throws ForbiddenError
250
+ */
251
+ export const deny = (message = 'Access denied') => {
252
+ return () => {
253
+ throw new ForbiddenError(message);
254
+ };
255
+ };
256
+
257
+ // Export all rules as an object for convenience
258
+ const rules = {
259
+ // Utility
260
+ resolvePath,
261
+ // Rule helpers
262
+ requireAuth,
263
+ requireRole,
264
+ requirePermission,
265
+ composeRules,
266
+ anyRule,
267
+ isOwner,
268
+ createRule,
269
+ allow,
270
+ deny,
271
+ };
272
+
273
+ export default rules;
274
+
@@ -0,0 +1,39 @@
1
+ import { GraphQLEnumType } from 'graphql';
2
+
3
+ const QLOperator = new GraphQLEnumType({
4
+ name: 'QLOperator',
5
+ values: {
6
+ EQ: {
7
+ value: 'EQ',
8
+ },
9
+ LT: {
10
+ value: 'LT',
11
+ },
12
+ GT: {
13
+ value: 'GT',
14
+ },
15
+ LTE: {
16
+ value: 'LTE',
17
+ },
18
+ GTE: {
19
+ value: 'GTE',
20
+ },
21
+ BTW: {
22
+ value: 'BTW',
23
+ },
24
+ NE: {
25
+ value: 'NE',
26
+ },
27
+ IN: {
28
+ value: 'IN',
29
+ },
30
+ NIN: {
31
+ value: 'NIN',
32
+ },
33
+ LIKE: {
34
+ value: 'LIKE',
35
+ },
36
+ },
37
+ });
38
+
39
+ export default QLOperator;