@simtlix/simfinity-js 2.4.0 → 2.4.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 CHANGED
@@ -20,12 +20,13 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
20
20
  - [Adding Middlewares](#adding-middlewares)
21
21
  - [Middleware Parameters](#middleware-parameters)
22
22
  - [Common Use Cases](#common-use-cases)
23
- - [Authorization Middleware](#-authorization-middleware)
23
+ - [Authorization](#-authorization)
24
24
  - [Quick Start](#quick-start-1)
25
25
  - [Permission Schema](#permission-schema)
26
26
  - [Rule Helpers](#rule-helpers)
27
27
  - [Policy Expressions (JSON AST)](#policy-expressions-json-ast)
28
- - [Integration with graphql-middleware](#integration-with-graphql-middleware)
28
+ - [Integration with GraphQL Yoga / Envelop](#integration-with-graphql-yoga--envelop)
29
+ - [Legacy: Integration with graphql-middleware](#legacy-integration-with-graphql-middleware)
29
30
  - [Relationships](#-relationships)
30
31
  - [Defining Relationships](#defining-relationships)
31
32
  - [Auto-Generated Resolve Methods](#auto-generated-resolve-methods)
@@ -75,7 +76,7 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
75
76
  - **Lifecycle Hooks**: Controller methods for granular control over operations
76
77
  - **Custom Validation**: Field-level and type-level custom validations
77
78
  - **Relationship Management**: Support for embedded and referenced relationships
78
- - **Authorization Middleware**: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, and declarative policy expressions
79
+ - **Authorization**: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, declarative policy expressions, and native Envelop/Yoga plugin support
79
80
 
80
81
  ## 📦 Installation
81
82
 
@@ -629,17 +630,17 @@ simfinity.use((params, next) => {
629
630
  5. **Performance consideration**: Middlewares run on every operation, keep them lightweight
630
631
  6. **Use context wisely**: Store request-specific data in the GraphQL context object
631
632
 
632
- ## 🔐 Authorization Middleware
633
+ ## 🔐 Authorization
633
634
 
634
- Simfinity.js provides a production-grade centralized GraphQL authorization middleware supporting RBAC/ABAC, function-based rules, declarative policy expressions (JSON AST), wildcard permissions, and configurable default policies.
635
+ Simfinity.js provides production-grade centralized GraphQL authorization supporting RBAC/ABAC, function-based rules, declarative policy expressions (JSON AST), wildcard permissions, and configurable default policies. It ships as a native Envelop plugin for GraphQL Yoga (recommended) and also supports the legacy graphql-middleware approach.
635
636
 
636
637
  ### Quick Start
637
638
 
638
639
  ```javascript
639
640
  const { auth } = require('@simtlix/simfinity-js');
640
- const { applyMiddleware } = require('graphql-middleware');
641
+ const { createYoga } = require('graphql-yoga');
641
642
 
642
- const { createAuthMiddleware, requireAuth, requireRole } = auth;
643
+ const { createAuthPlugin, requireAuth, requireRole } = auth;
643
644
 
644
645
  // Define your permission schema
645
646
  const permissions = {
@@ -665,9 +666,9 @@ const permissions = {
665
666
  },
666
667
  };
667
668
 
668
- // Create and apply the middleware
669
- const authMiddleware = createAuthMiddleware(permissions, { defaultPolicy: 'DENY' });
670
- const schemaWithAuth = applyMiddleware(schema, authMiddleware);
669
+ // Create the Envelop auth plugin and pass it to your server
670
+ const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
671
+ const yoga = createYoga({ schema, plugins: [authPlugin] });
671
672
  ```
672
673
 
673
674
  ### Permission Schema
@@ -869,25 +870,24 @@ Use `{ ref: 'path' }` to reference values:
869
870
  - Unknown operators fail closed (deny)
870
871
  - No `eval()` or `Function()` - pure object traversal
871
872
 
872
- ### Integration with graphql-middleware
873
+ ### Integration with GraphQL Yoga / Envelop
873
874
 
874
- The auth middleware integrates with the `graphql-middleware` package:
875
+ The recommended way to use the auth system is via the Envelop plugin, which works natively with GraphQL Yoga and any Envelop-based server. The plugin wraps resolvers in-place without rebuilding the schema, avoiding compatibility issues.
875
876
 
876
877
  ```javascript
877
- const express = require('express');
878
- const { graphqlHTTP } = require('express-graphql');
879
- const { applyMiddleware } = require('graphql-middleware');
878
+ const { createYoga } = require('graphql-yoga');
879
+ const { createServer } = require('http');
880
880
  const simfinity = require('@simtlix/simfinity-js');
881
881
 
882
882
  const { auth } = simfinity;
883
- const { createAuthMiddleware, requireAuth, requireRole, requirePermission } = auth;
883
+ const { createAuthPlugin, requireAuth, requireRole, requirePermission } = auth;
884
884
 
885
885
  // Define your types and connect them
886
886
  simfinity.connect(null, UserType, 'user', 'users');
887
887
  simfinity.connect(null, PostType, 'post', 'posts');
888
888
 
889
889
  // Create base schema
890
- const baseSchema = simfinity.createSchema();
890
+ const schema = simfinity.createSchema();
891
891
 
892
892
  // Define permissions
893
893
  const permissions = {
@@ -921,36 +921,51 @@ const permissions = {
921
921
  },
922
922
  };
923
923
 
924
- // Create auth middleware
925
- const authMiddleware = createAuthMiddleware(permissions, {
926
- defaultPolicy: 'DENY', // Deny access when no rule matches
927
- debug: false, // Enable for debugging
924
+ // Create auth plugin
925
+ const authPlugin = createAuthPlugin(permissions, {
926
+ defaultPolicy: 'DENY',
927
+ debug: false,
928
928
  });
929
929
 
930
- // Apply middleware to schema
931
- const schema = applyMiddleware(baseSchema, authMiddleware);
932
-
933
- // Setup Express with context
934
- const app = express();
935
-
936
- app.use('/graphql', graphqlHTTP((req) => ({
930
+ // Setup Yoga with the auth plugin
931
+ const yoga = createYoga({
937
932
  schema,
938
- graphiql: true,
939
- context: {
940
- user: req.user, // Set by your authentication middleware
941
- },
942
- formatError: simfinity.buildErrorFormatter((err) => {
943
- console.error(err);
933
+ plugins: [authPlugin],
934
+ context: (req) => ({
935
+ user: req.user, // Set by your authentication layer
944
936
  }),
945
- })));
937
+ });
946
938
 
947
- app.listen(4000);
939
+ const server = createServer(yoga);
940
+ server.listen(4000);
941
+ ```
942
+
943
+ ### Legacy: Integration with graphql-middleware
944
+
945
+ > **Deprecated:** `applyMiddleware` from `graphql-middleware` rebuilds the schema via `mapSchema`,
946
+ > which can cause `"Schema must contain uniquely named types"` errors with Simfinity schemas.
947
+ > Use `createAuthPlugin` with GraphQL Yoga / Envelop instead.
948
+
949
+ ```javascript
950
+ const { applyMiddleware } = require('graphql-middleware');
951
+ const simfinity = require('@simtlix/simfinity-js');
952
+
953
+ const { auth } = simfinity;
954
+ const { createAuthMiddleware, requireAuth, requireRole } = auth;
955
+
956
+ const baseSchema = simfinity.createSchema();
957
+
958
+ const authMiddleware = createAuthMiddleware(permissions, {
959
+ defaultPolicy: 'DENY',
960
+ });
961
+
962
+ const schema = applyMiddleware(baseSchema, authMiddleware);
948
963
  ```
949
964
 
950
- ### Middleware Options
965
+ ### Plugin / Middleware Options
951
966
 
952
967
  ```javascript
953
- const middleware = createAuthMiddleware(permissions, {
968
+ const plugin = createAuthPlugin(permissions, {
954
969
  defaultPolicy: 'DENY', // 'ALLOW' or 'DENY' (default: 'DENY')
955
970
  debug: false, // Enable debug logging
956
971
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/auth/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Simfinity GraphQL Authorization Middleware
2
+ * Simfinity GraphQL Authorization
3
3
  *
4
4
  * Production-grade centralized GraphQL authorization supporting:
5
5
  * - RBAC / ABAC
@@ -10,9 +10,9 @@
10
10
  *
11
11
  * @example
12
12
  * import { auth } from '@simtlix/simfinity-js';
13
- * import { applyMiddleware } from 'graphql-middleware';
13
+ * import { createYoga } from 'graphql-yoga';
14
14
  *
15
- * const { createAuthMiddleware, requireAuth, requireRole } = auth;
15
+ * const { createAuthPlugin, requireAuth, requireRole } = auth;
16
16
  *
17
17
  * const permissions = {
18
18
  * Query: {
@@ -28,10 +28,11 @@
28
28
  * }
29
29
  * };
30
30
  *
31
- * const authMiddleware = createAuthMiddleware(permissions, { defaultPolicy: 'DENY' });
32
- * const schemaWithAuth = applyMiddleware(schema, authMiddleware);
31
+ * const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
32
+ * const yoga = createYoga({ schema, plugins: [authPlugin] });
33
33
  */
34
34
 
35
+ import { GraphQLObjectType, defaultFieldResolver } from 'graphql';
35
36
  import { UnauthenticatedError, ForbiddenError, createAuthError } from './errors.js';
36
37
  import { isPolicyExpression, createRuleFromExpression, evaluateExpression } from './expressions.js';
37
38
  import {
@@ -169,33 +170,14 @@ const executeRule = async (rule, parent, args, ctx, info) => {
169
170
  };
170
171
 
171
172
  /**
172
- * Creates a graphql-middleware compatible authorization middleware
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.
173
177
  *
174
178
  * @param {PermissionSchema} permissions - The permission schema object
175
179
  * @param {AuthMiddlewareOptions} [options={}] - Middleware options
176
180
  * @returns {Function} A graphql-middleware compatible middleware function
177
- *
178
- * @example
179
- * const permissions = {
180
- * Query: {
181
- * users: requireAuth(),
182
- * adminDashboard: requireRole('ADMIN')
183
- * },
184
- * User: {
185
- * '*': requireAuth(),
186
- * email: requireRole('ADMIN')
187
- * },
188
- * Post: {
189
- * content: {
190
- * anyOf: [
191
- * { eq: [{ ref: 'parent.published' }, true] },
192
- * { eq: [{ ref: 'parent.authorId' }, { ref: 'ctx.user.id' }] }
193
- * ]
194
- * }
195
- * }
196
- * };
197
- *
198
- * const middleware = createAuthMiddleware(permissions, { defaultPolicy: 'DENY' });
199
181
  */
200
182
  export const createAuthMiddleware = (permissions, options = {}) => {
201
183
  const {
@@ -250,8 +232,11 @@ export const createAuthMiddleware = (permissions, options = {}) => {
250
232
  };
251
233
 
252
234
  /**
253
- * Creates a field-level middleware object from a permission schema
254
- * This can be used with graphql-middleware's applyMiddleware
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.
255
240
  *
256
241
  * @param {PermissionSchema} permissions - The permission schema
257
242
  * @param {AuthMiddlewareOptions} [options={}] - Middleware options
@@ -277,9 +262,103 @@ export const createFieldMiddleware = (permissions, options = {}) => {
277
262
  return fieldMiddleware;
278
263
  };
279
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
+
280
358
  // Default export with all auth utilities
281
359
  const auth = {
282
- // Main factory
360
+ // Main factories
361
+ createAuthPlugin,
283
362
  createAuthMiddleware,
284
363
  createFieldMiddleware,
285
364
 
package/src/index.js CHANGED
@@ -63,6 +63,7 @@ const FieldExtensionsType = new GraphQLObjectType({
63
63
  fields: () => ({
64
64
  relation: { type: RelationType },
65
65
  stateMachine: { type: GraphQLBoolean },
66
+ readOnly: { type: GraphQLBoolean },
66
67
  }),
67
68
  });
68
69
 
package/src/plugins.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { createAuthPlugin } from './auth/index.js';
2
+
3
+ export { createAuthPlugin } from './auth/index.js';
4
+
1
5
  /**
2
6
  * Apollo Server plugin to add count to GraphQL response extensions
3
7
  * @returns {Object} Apollo Server plugin
@@ -40,11 +44,10 @@ export const envelopCountPlugin = () => {
40
44
  };
41
45
  };
42
46
 
43
- // Export all plugins as an object for convenience
44
47
  const plugins = {
48
+ createAuthPlugin,
45
49
  apolloCountPlugin,
46
50
  envelopCountPlugin,
47
51
  };
48
52
 
49
53
  export default plugins;
50
-