@simtlix/simfinity-js 2.4.1 → 2.4.4

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.
@@ -0,0 +1,71 @@
1
+ ---
2
+ description: Simfinity.js project architecture overview -- always loaded for context
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Simfinity.js Architecture
7
+
8
+ `@simtlix/simfinity-js` is a GraphQL framework that auto-generates schemas, CRUD queries, mutations, and MongoDB models from `GraphQLObjectType` definitions.
9
+
10
+ ## Tech Stack
11
+
12
+ - **Language**: JavaScript (ES modules, `"type": "module"`)
13
+ - **Runtime**: Node.js >= 18.18.0
14
+ - **GraphQL**: `graphql` ^16 (peer dependency)
15
+ - **Database**: MongoDB via `mongoose` ^8 (peer dependency)
16
+ - **Testing**: Vitest
17
+ - **Linting**: ESLint 9 (flat config)
18
+ - **Server integration**: GraphQL Yoga with Envelop plugins
19
+
20
+ ## Core Flow
21
+
22
+ ```
23
+ Define GraphQLObjectType
24
+ |
25
+ v
26
+ simfinity.connect(null, Type, 'singular', 'plural', controller, onModelCreated, stateMachine)
27
+ -- or --
28
+ simfinity.addNoEndpointType(Type) // supporting types without endpoints
29
+ |
30
+ v
31
+ simfinity.createSchema()
32
+ |
33
+ +-- buildPendingInputTypes() // resolve deferred input types
34
+ +-- generateModel() // create Mongoose schema + model per type
35
+ +-- autoGenerateResolvers() // wire relationship resolvers
36
+ +-- buildRootQuery() // list, single-entity, aggregate queries
37
+ +-- buildMutation() // add, update, delete, state-change mutations
38
+ |
39
+ v
40
+ GraphQLSchema ready for Yoga/Apollo
41
+ ```
42
+
43
+ ## Module Map
44
+
45
+ | File | Responsibility |
46
+ |------|---------------|
47
+ | `src/index.js` | Core engine: type registration, schema/model generation, query/mutation builders, resolvers, middleware, scope |
48
+ | `src/auth/index.js` | Auth plugin (`createAuthPlugin`), permission resolution, deprecated middleware |
49
+ | `src/auth/rules.js` | Rule helpers: `requireAuth`, `requireRole`, `composeRules`, `isOwner`, `allow`, `deny` |
50
+ | `src/auth/expressions.js` | Policy expression DSL evaluator |
51
+ | `src/auth/errors.js` | `UnauthenticatedError`, `ForbiddenError` (extend `SimfinityError`) |
52
+ | `src/validators.js` | Declarative field validators: `stringLength`, `pattern`, `email`, `numberRange`, etc. |
53
+ | `src/scalars.js` | Pre-built scalars (`EmailScalar`, `URLScalar`) and scalar factories |
54
+ | `src/plugins.js` | Plugin exports: `createAuthPlugin`, `apolloCountPlugin`, `envelopCountPlugin` |
55
+ | `src/const/QLOperator.js` | `GraphQLEnumType` for filter operators (EQ, LT, GT, IN, LIKE, etc.) |
56
+ | `src/const/QLValue.js` | `GraphQLScalarType` for filter values |
57
+ | `src/const/QLSort.js` | `GraphQLInputObjectType` for sort expressions |
58
+ | `src/errors/simfinity.error.js` | Base error class with `code` and `status` extensions |
59
+ | `src/errors/internal-server.error.js` | Internal server error (extends `SimfinityError`) |
60
+
61
+ ## Key Global State (in `src/index.js`)
62
+
63
+ - `typesDict` -- primary registry: `{ types: { [typeName]: { model, gqltype, simpleEntityEndpointName, listEntitiesEndpointName, endpoint, controller, stateMachine, inputType } } }`
64
+ - `typesDictForUpdate` -- parallel registry with update-specific input types
65
+ - `waitingInputType` -- deferred input types awaiting dependency resolution (circular refs)
66
+ - `registeredMutations` -- custom mutations registered via `registerMutation()`
67
+ - `middlewares` -- array of global middleware functions registered via `use()`
68
+
69
+ ## Introspection Extension (Critical)
70
+
71
+ Simfinity monkey-patches `__Field._fields` to inject `FieldExtensionsType` and `RelationType` into GraphQL introspection globally. This means these types exist outside the schema's type map. Any tool that rebuilds or copies the schema (like `graphql-middleware`'s `applyMiddleware` / `mapSchema`) will cause duplicate type errors. Always use Envelop plugins for schema transformations.
@@ -0,0 +1,100 @@
1
+ ---
2
+ description: Documents the authorization subsystem -- Envelop plugin, permission maps, rule helpers, and policy expressions
3
+ globs: src/auth/**
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Authorization Module (`src/auth/`)
8
+
9
+ ## File Structure
10
+
11
+ - `index.js` -- `createAuthPlugin` (Envelop), deprecated `createAuthMiddleware`, permission resolution logic
12
+ - `rules.js` -- Rule helper functions: `requireAuth`, `requireRole`, `requirePermission`, `composeRules`, `anyRule`, `isOwner`, `allow`, `deny`, `createRule`
13
+ - `expressions.js` -- Policy expression DSL: `evaluateExpression`, `isPolicyExpression`, `createRuleFromExpression`
14
+ - `errors.js` -- `UnauthenticatedError`, `ForbiddenError` (extend `SimfinityError`)
15
+
16
+ ## `createAuthPlugin(permissions, options)` -- Envelop Plugin
17
+
18
+ The recommended auth integration. Returns an Envelop plugin with an `onSchemaChange` hook.
19
+
20
+ ```javascript
21
+ const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'DENY', debug: false });
22
+ // Use with GraphQL Yoga:
23
+ const yoga = createYoga({ schema, plugins: [authPlugin] });
24
+ ```
25
+
26
+ **How it works:**
27
+ 1. On `onSchemaChange({ schema })`, iterates all types in `schema.getTypeMap()`
28
+ 2. Skips non-`GraphQLObjectType` types and `__`-prefixed introspection types
29
+ 3. For each field, looks up rules from the permission map via `getFieldRules()`
30
+ 4. Wraps `field.resolve` in-place (mutates the schema, no rebuild)
31
+ 5. Uses a `WeakSet` to prevent double-wrapping if `onSchemaChange` fires again
32
+
33
+ **`defaultPolicy`**: `'DENY'` (default) blocks fields with no rules; `'ALLOW'` permits them.
34
+
35
+ ## Permission Map Structure
36
+
37
+ ```javascript
38
+ const permissions = {
39
+ Query: {
40
+ '*': allow(), // wildcard: default for all Query fields
41
+ series: requireAuth(), // specific field overrides wildcard
42
+ },
43
+ Mutation: {
44
+ addserie: requireRole('admin'),
45
+ deleteserie: composeRules(requireAuth(), requireRole('admin')),
46
+ },
47
+ Serie: {
48
+ title: allow(),
49
+ internalNotes: requireRole('admin'),
50
+ },
51
+ };
52
+ ```
53
+
54
+ - Keys are GraphQL type names (must match `GraphQLObjectType.name` exactly)
55
+ - Field keys are the auto-generated endpoint names (lowercase)
56
+ - Wildcard `'*'` applies to all fields of a type unless overridden by an exact match
57
+ - Values can be: a rule function, an array of rule functions (AND logic), or a policy expression string
58
+
59
+ ## Rule Resolution (`getFieldRules`)
60
+
61
+ 1. Check `permissions[typeName][fieldName]` (exact match)
62
+ 2. Fall back to `permissions[typeName]['*']` (wildcard)
63
+ 3. If neither exists, return `null` (applies `defaultPolicy`)
64
+
65
+ Each rule receives `(parent, args, ctx, info)` and must return `true` or throw an error.
66
+
67
+ ## Rule Helpers (from `rules.js`)
68
+
69
+ | Helper | Description |
70
+ |--------|-------------|
71
+ | `requireAuth(userPath?)` | Checks `ctx[userPath]` exists (default: `'user'`) |
72
+ | `requireRole(role, options?)` | Checks user has role. Options: `{ userPath, rolesPath }` |
73
+ | `requirePermission(perm, options?)` | Checks user has permission string |
74
+ | `composeRules(...rules)` | AND: all rules must pass |
75
+ | `anyRule(...rules)` | OR: at least one rule must pass |
76
+ | `isOwner(ownerField, userIdField?, options?)` | Compares `parent[ownerField]` with user ID |
77
+ | `createRule(predicate, errorMessage?, errorCode?)` | Custom rule from predicate function |
78
+ | `allow()` | Always returns `true` |
79
+ | `deny(message?)` | Always throws `ForbiddenError` |
80
+
81
+ ## Policy Expressions (from `expressions.js`)
82
+
83
+ String-based alternative to function rules. Evaluated by `evaluateExpression()`.
84
+
85
+ ```javascript
86
+ const permissions = {
87
+ Mutation: {
88
+ deleteserie: 'ROLE:admin',
89
+ updateserie: 'AUTH AND ROLE:editor',
90
+ addserie: 'ROLE:admin OR ROLE:editor',
91
+ },
92
+ };
93
+ ```
94
+
95
+ `createRuleFromExpression()` converts an expression string into a standard rule function.
96
+
97
+ ## Deprecated APIs
98
+
99
+ - `createAuthMiddleware()` -- uses `graphql-middleware`'s `applyMiddleware` which causes duplicate type errors with Simfinity's introspection patch. Use `createAuthPlugin` instead.
100
+ - `createFieldMiddleware()` -- same issue. Use `createAuthPlugin` instead.
@@ -0,0 +1,62 @@
1
+ ---
2
+ description: Coding standards and best practices for writing new Simfinity.js code
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Coding Standards
7
+
8
+ ## Module System
9
+
10
+ - Use ES module syntax (`import`/`export`). No CommonJS (`require`/`module.exports`).
11
+ - All imports at the top of the file. No inline or dynamic imports inside functions.
12
+
13
+ ## Style (enforced by ESLint)
14
+
15
+ - Single quotes, semicolons, trailing commas in multiline constructs.
16
+ - `prefer-const` -- use `const` unless reassignment is needed.
17
+ - No `var`, no `for...in` loops. Use `Object.entries()`, `Object.keys()`, or `for...of`.
18
+ - `no-param-reassign` with `props: false` -- reassigning parameter properties is allowed (common in resolvers and `materializeModel`).
19
+ - `no-underscore-dangle` is off -- `_id` and `_fields` are used throughout for MongoDB and GraphQL internals.
20
+
21
+ ## Error Handling
22
+
23
+ - Extend `SimfinityError` for all domain errors. Always provide `message`, `code` (string), and `status` (HTTP number):
24
+
25
+ ```javascript
26
+ throw new SimfinityError('Resource not found', 'NOT_FOUND', 404);
27
+ ```
28
+
29
+ - For auth errors, use `UnauthenticatedError` (401) or `ForbiddenError` (403) from `src/auth/errors.js`.
30
+ - Wrap mutation logic in Mongoose transactions. Handle `TransientTransactionError` with retry (see `executeOperation` pattern).
31
+
32
+ ## GraphQL Type Definitions
33
+
34
+ - Always use `extensions.relation` on fields referencing other `GraphQLObjectType`s. Omitting it causes warnings and broken input types.
35
+ - Use `GraphQLNonNull` wrapper for required fields.
36
+ - Use `extensions.readOnly` for computed/system fields that should not appear in input types.
37
+ - Validated scalars: use `createValidatedScalar(name, description, baseScalarType, validate)` -- the resulting scalar name is `{name}_{baseScalarType.name}`.
38
+
39
+ ## Schema Transformations
40
+
41
+ - **Never** use `graphql-middleware`'s `applyMiddleware` or `@graphql-tools/utils`'s `mapSchema` on a Simfinity schema. These rebuild the schema, duplicating the globally-injected `FieldExtensionsType`.
42
+ - Use Envelop plugins (`onSchemaChange` hook) to wrap resolvers in-place.
43
+
44
+ ## Export Conventions
45
+
46
+ - Named exports for public API functions.
47
+ - Default export as an object aggregating all named exports (for convenient destructuring).
48
+ - Re-export submodule defaults from `src/index.js`: `validators`, `scalars`, `plugins`, `auth`.
49
+
50
+ ## Function Naming
51
+
52
+ - Internal helpers: camelCase, not exported.
53
+ - Public API: camelCase, exported with `export const`.
54
+ - Deprecated functions: mark with `@deprecated` JSDoc tag.
55
+
56
+ ## Keeping Project Artifacts in Sync
57
+
58
+ When making code changes, update **all** related artifacts:
59
+
60
+ 1. **Tests** (`tests/`): add or update tests for any new or changed behavior. See `simfinity-testing.mdc`.
61
+ 2. **Documentation** (`README.md`): update examples, API docs, and Table of Contents for any public API change. See `simfinity-documentation.mdc`.
62
+ 3. **Cursor rules** (`.cursor/rules/`): if a change affects architecture, internal functions, the extensions system, the auth module, coding standards, or testing/documentation conventions, update the corresponding `.mdc` rule file so it stays accurate.
@@ -0,0 +1,82 @@
1
+ ---
2
+ description: Documents the critical internal functions and pipelines in the Simfinity core engine
3
+ globs: src/index.js
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Core Functions in src/index.js
8
+
9
+ ## Registration Phase
10
+
11
+ ### `connect(model, gqltype, simpleEntityEndpointName, listEntitiesEndpointName, controller, onModelCreated, stateMachine)`
12
+
13
+ Registers a GraphQL type for full CRUD endpoint generation. The `model` param is typically `null` (generated later by `createSchema`). Stores metadata in `typesDict` and `typesDictForUpdate`.
14
+
15
+ ### `addNoEndpointType(gqltype)`
16
+
17
+ Registers a supporting type (no query/mutation endpoints). Auto-detects whether a Mongoose model is needed based on relationship fields. Used for embedded types or types only referenced by other types.
18
+
19
+ ### `registerMutation(name, description, inputModel, outputModel, callback)`
20
+
21
+ Registers a custom mutation outside the standard CRUD pattern.
22
+
23
+ ## Schema Build Phase (`createSchema()`)
24
+
25
+ Called after all `connect()`/`addNoEndpointType()` calls. Executes in order:
26
+
27
+ 1. **`generateModel()`** / **`generateModelWithoutCollection()`** -- creates Mongoose schemas via `generateSchemaDefinition()`, auto-indexes all ObjectId fields via `createSchemaWithIndexes()` / `findObjectIdFields()`.
28
+ 2. **`buildPendingInputTypes()`** -- recursively resolves deferred input types (handles circular dependencies by retrying until all resolve).
29
+ 3. **`autoGenerateResolvers()`** -- for each type's fields with `extensions.relation` and no existing resolver, generates: one-to-one resolvers (`findById`), one-to-many resolvers (aggregation pipeline with filter support).
30
+ 4. **`buildRootQuery()`** -- creates the root Query type with: `{singular}(id)` single-entity query, `{plural}(filters, pagination, sort)` list query, `{plural}_aggregate(filters, aggregation)` aggregation query.
31
+ 5. **`buildMutation()`** -- creates the Mutation type with: `add{singular}(input)`, `update{singular}(input)`, `delete{singular}(id)`, `{actionName}_{singular}(input)` for state machine transitions.
32
+
33
+ ## Auto-Generated Endpoint Naming
34
+
35
+ - **Single entity query**: `simpleEntityEndpointName` (e.g., `serie`)
36
+ - **List query**: `listEntitiesEndpointName` (e.g., `series`)
37
+ - **Add mutation**: `add` + `simpleEntityEndpointName` (e.g., `addserie`)
38
+ - **Update mutation**: `update` + `simpleEntityEndpointName` (e.g., `updateserie`)
39
+ - **Delete mutation**: `delete` + `simpleEntityEndpointName` (e.g., `deleteserie`)
40
+ - **State action**: `{actionName}_{simpleEntityEndpointName}` (e.g., `approve_serie`)
41
+
42
+ All names are lowercase as provided by the consumer in `connect()`.
43
+
44
+ ## Input Type Generation (`buildInputType()`)
45
+
46
+ Creates two input types per registered type:
47
+
48
+ - `{Type}Input` -- for `add` mutations (excludes `id`, readOnly fields, state-machine-managed `state`)
49
+ - `{Type}InputForUpdate` -- for `update` mutations (all fields optional except `id` which is required)
50
+
51
+ Handles: scalars, enums, relation fields (`IdInputType` for refs, nested input for embedded), `GraphQLList` fields (`OneToMany{A|U}{fieldName}` with `added`/`updated`/`deleted` sub-fields), self-referencing collections.
52
+
53
+ ## Query Pipeline
54
+
55
+ `buildQuery(args, gqltype)` translates GraphQL filter arguments into a MongoDB aggregation pipeline:
56
+
57
+ - `buildQueryTerms()` -- converts scalar/enum filters to `$match`, object/relation filters to `$lookup` + `$unwind` + `$match`
58
+ - `buildAggregationsForSort()` -- adds `$lookup`/`$unwind` for sort on related fields
59
+ - Pagination: `$skip` + `$limit` from `QLPagination` input
60
+ - Sort: `$sort` from `QLSortExpression` input
61
+
62
+ `buildAggregationQuery()` extends this with `$group` for aggregation operations (SUM, COUNT, AVG, MIN, MAX).
63
+
64
+ ## Mutation Pipeline
65
+
66
+ `executeOperation()` wraps all mutations in Mongoose transactions:
67
+
68
+ 1. Starts a session + transaction
69
+ 2. Calls the appropriate handler: `onSaveObject`, `onUpdateSubject`, `onDeleteObject`, `onStateChanged`
70
+ 3. Commits on success, aborts on error
71
+ 4. Auto-retries on `TransientTransactionError`
72
+
73
+ `materializeModel()` transforms GraphQL input args into Mongoose-compatible documents, running field-level and type-level validators during the process.
74
+
75
+ ## Middleware and Scope
76
+
77
+ - `use(middleware)` -- pushes to global `middlewares` array. `excecuteMiddleware()` chains them with `next()` pattern.
78
+ - `executeScope()` -- checks `type.gqltype.extensions.scope[operation]` and calls the scope function to apply row-level filtering for `find`, `get_by_id`, and `aggregate` operations.
79
+
80
+ ## Introspection Monkey-Patch
81
+
82
+ Lines 52-85: `FieldExtensionsType` and `RelationType` are injected into `__Field._fields` globally. This enables introspection clients to discover relation metadata and state machine flags. **Never rebuild the schema** with tools that clone types (e.g., `mapSchema` from `@graphql-tools/utils`) -- it will duplicate these globally-injected types.
@@ -0,0 +1,59 @@
1
+ ---
2
+ description: Rules for maintaining README.md documentation when code changes
3
+ globs: README.md
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Documentation Maintenance
8
+
9
+ ## When to Update README.md
10
+
11
+ - **New public API**: document the function signature, purpose, and a usage example.
12
+ - **Changed behavior**: update relevant sections and code examples to reflect the change.
13
+ - **New feature**: add a new section in the appropriate position (see section order below).
14
+ - **Removed/deprecated feature**: mark as deprecated with migration guidance; do not silently remove.
15
+
16
+ ## Section Order
17
+
18
+ Maintain this logical flow (define -> operate -> protect -> advanced):
19
+
20
+ 1. Overview / Quick Start
21
+ 2. Schema Definition (defining `GraphQLObjectType` with Simfinity)
22
+ 3. Relationships (embedded, non-embedded, one-to-many)
23
+ 4. Validations (field-level, type-level, declarative validators)
24
+ 5. Queries (single entity, list, filtering, pagination, sorting)
25
+ 6. Mutations (add, update, delete)
26
+ 7. State Machines (states, transitions, actions)
27
+ 8. Controllers & Lifecycle Hooks (onSaving, onSaved, onUpdating, onUpdated, onDelete)
28
+ 9. Query Scope (row-level security via scope functions)
29
+ 10. Authorization (Envelop auth plugin, permission maps, rule helpers)
30
+ 11. Middlewares (global middleware via `use()`)
31
+ 12. Plugins (count plugins, auth plugin re-export)
32
+ 13. Custom Scalars (pre-built and factory functions)
33
+ 14. Error Handling
34
+ 15. Resources (links to example projects, API docs)
35
+
36
+ ## Table of Contents
37
+
38
+ Update the Table of Contents whenever sections are added, removed, or reordered. Ensure every `##` and `###` heading has a corresponding ToC entry.
39
+
40
+ ## Naming in Code Examples
41
+
42
+ Use Simfinity's auto-generated endpoint names consistently:
43
+
44
+ - **Queries**: `{plural}` for list (e.g., `series`), `{singular}` for single entity (e.g., `serie`)
45
+ - **Mutations**: `add{singular}` (e.g., `addserie`), `update{singular}` (e.g., `updateserie`), `delete{singular}` (e.g., `deleteserie`)
46
+ - **State actions**: `{actionName}_{singular}` (e.g., `approve_serie`)
47
+ - **Aggregation**: `{plural}_aggregate` (e.g., `series_aggregate`)
48
+
49
+ All names are lowercase as defined in `connect()` calls.
50
+
51
+ ## Reference Project
52
+
53
+ Always link to the [Series Sample Project](https://github.com/simtlix/series-sample) as a complete working example. Mention it in Quick Start and Resources sections.
54
+
55
+ ## Style Guidelines
56
+
57
+ - Use fenced code blocks with `javascript` language tag for all code examples.
58
+ - Keep examples realistic -- use types and field names from the series-sample project (Serie, Season, Star, etc.) rather than abstract names.
59
+ - Show both the type definition and the resulting GraphQL operation where appropriate.
@@ -0,0 +1,121 @@
1
+ ---
2
+ description: Documents the field/type extensions metadata system used in GraphQLObjectType definitions
3
+ globs: src/index.js
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Extensions Metadata System
8
+
9
+ Simfinity uses the standard GraphQL `extensions` property on fields and types to carry metadata that drives schema generation, model creation, resolver wiring, and validation.
10
+
11
+ ## Field-Level Extensions
12
+
13
+ ### `extensions.relation`
14
+
15
+ Defines relationships between types. Required on every field whose type is another `GraphQLObjectType` or `GraphQLList` of one.
16
+
17
+ ```javascript
18
+ fields: () => ({
19
+ author: {
20
+ type: AuthorType,
21
+ extensions: {
22
+ relation: {
23
+ embedded: false, // false = ObjectId reference, true = nested document
24
+ connectionField: 'author_id', // MongoDB field storing the ObjectId (defaults to field name)
25
+ displayField: 'name', // field shown in introspection UI hints
26
+ },
27
+ },
28
+ },
29
+ chapters: {
30
+ type: new GraphQLList(ChapterType),
31
+ extensions: {
32
+ relation: {
33
+ embedded: true, // stored inline in parent document
34
+ },
35
+ },
36
+ },
37
+ })
38
+ ```
39
+
40
+ - **Non-embedded** (`embedded: false`): stored as ObjectId in MongoDB. Input types use `IdInputType` (`{ id: String! }`). List fields generate `OneToMany` input with `added`/`updated`/`deleted` sub-fields. Auto-generates `$lookup` resolvers.
41
+ - **Embedded** (`embedded: true`): stored inline. Input types use the nested type's full input type. No `$lookup` needed.
42
+ - **`connectionField`**: the actual MongoDB field name for the ObjectId reference. If omitted, defaults to the GraphQL field name.
43
+
44
+ ### `extensions.readOnly`
45
+
46
+ ```javascript
47
+ createdAt: {
48
+ type: GraphQLString,
49
+ extensions: { readOnly: true },
50
+ }
51
+ ```
52
+
53
+ Excludes the field from both add and update input types. The field can only be set programmatically (e.g., in a controller hook).
54
+
55
+ ### `extensions.unique`
56
+
57
+ ```javascript
58
+ email: {
59
+ type: GraphQLString,
60
+ extensions: { unique: true },
61
+ }
62
+ ```
63
+
64
+ Adds a MongoDB unique index on the field in `generateSchemaDefinition()`.
65
+
66
+ ### `extensions.validations`
67
+
68
+ ```javascript
69
+ name: {
70
+ type: new GraphQLNonNull(GraphQLString),
71
+ extensions: {
72
+ validations: validators.stringLength('Name', 2, 100),
73
+ // Returns: { CREATE: [validator], UPDATE: [validator] }
74
+ },
75
+ }
76
+ ```
77
+
78
+ Validators are executed during `materializeModel()` before saving/updating. Each validator has a `.validate(typeName, fieldName, value, session)` method. The `CREATE` array runs on add, `UPDATE` on update.
79
+
80
+ ### `extensions.stateMachine` (auto-set)
81
+
82
+ Not set manually. When `buildInputType()` detects a `state` field on a type that has a state machine, it sets `extensions.stateMachine = true` on that field and excludes it from input types.
83
+
84
+ ## Type-Level Extensions
85
+
86
+ ### `extensions.validations` (on the type)
87
+
88
+ ```javascript
89
+ const BookType = new GraphQLObjectType({
90
+ name: 'Book',
91
+ extensions: {
92
+ validations: {
93
+ CREATE: [{ validate: async (typeName, args, modelArgs, session) => { /* cross-field validation */ } }],
94
+ UPDATE: [{ validate: async (typeName, args, modelArgs, session) => { /* ... */ } }],
95
+ },
96
+ },
97
+ fields: () => ({ /* ... */ }),
98
+ });
99
+ ```
100
+
101
+ Type-level validators run after all field-level validators, receiving the full args and materialized model.
102
+
103
+ ### `extensions.scope`
104
+
105
+ ```javascript
106
+ const BookType = new GraphQLObjectType({
107
+ name: 'Book',
108
+ extensions: {
109
+ scope: {
110
+ find: async ({ type, args, operation, context }) => {
111
+ args.owner_id = { operator: 'EQ', value: context.user.id };
112
+ },
113
+ get_by_id: async ({ type, args, operation, context }) => { /* ... */ },
114
+ aggregate: async ({ type, args, operation, context }) => { /* ... */ },
115
+ },
116
+ },
117
+ fields: () => ({ /* ... */ }),
118
+ });
119
+ ```
120
+
121
+ Scope functions modify query args in-place to enforce row-level security. Called by `executeScope()` before building the MongoDB aggregation pipeline.
@@ -0,0 +1,90 @@
1
+ ---
2
+ description: Testing conventions, patterns, and requirements for when tests must be updated
3
+ globs: tests/**
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Testing Conventions
8
+
9
+ ## Framework and Setup
10
+
11
+ - **Vitest** with ES module imports: `import { describe, test, expect, beforeAll } from 'vitest';`
12
+ - Run tests: `npm test` (runs `vitest run`), `npm run test:watch`, `npm run test:coverage`
13
+ - Call `simfinity.preventCreatingCollection(true)` in `beforeAll` to prevent MongoDB collection creation during tests.
14
+
15
+ ## Test File Naming
16
+
17
+ - `tests/{module}.test.js` corresponding to `src/{module}.js`
18
+ - Auth tests: `tests/auth.test.js` covers `src/auth/index.js`, `src/auth/rules.js`, `src/auth/expressions.js`
19
+ - Feature-specific tests: `tests/aggregation.test.js`, `tests/scalar-naming.test.js`, etc.
20
+
21
+ ## Test Structure
22
+
23
+ ```javascript
24
+ describe('Module or Feature Name', () => {
25
+ beforeAll(() => {
26
+ simfinity.preventCreatingCollection(true);
27
+ });
28
+
29
+ describe('function or sub-feature', () => {
30
+ test('should describe expected behavior', () => {
31
+ // Arrange, Act, Assert
32
+ });
33
+ });
34
+ });
35
+ ```
36
+
37
+ - Group by module, then by function/feature.
38
+ - Mirror the export structure: if a module exports `{ foo, bar }`, have `describe('foo', ...)` and `describe('bar', ...)`.
39
+ - Test export existence: verify all expected exports are defined.
40
+
41
+ ## When to Update Tests
42
+
43
+ **Any change to `src/` must have corresponding test updates:**
44
+
45
+ - **New export**: add a test verifying the export exists and has the expected type.
46
+ - **New function/feature**: add both positive cases (happy path) and negative cases (error handling, edge cases).
47
+ - **Changed behavior**: update existing tests to reflect the new behavior. Add tests for any new code paths.
48
+ - **Bug fix**: add a regression test that would have caught the bug.
49
+ - **Deprecated function**: keep existing tests but add a note. Do not remove tests for deprecated functions that still exist.
50
+
51
+ ## Common Test Patterns
52
+
53
+ ### Testing GraphQL types without MongoDB
54
+
55
+ ```javascript
56
+ const TestType = new GraphQLObjectType({
57
+ name: 'TestType',
58
+ fields: () => ({
59
+ id: { type: GraphQLID },
60
+ name: { type: GraphQLString },
61
+ }),
62
+ });
63
+
64
+ const schema = new GraphQLSchema({
65
+ query: new GraphQLObjectType({
66
+ name: 'Query',
67
+ fields: { test: { type: TestType, resolve: () => ({ id: '1', name: 'test' }) } },
68
+ }),
69
+ });
70
+ ```
71
+
72
+ ### Testing auth rules
73
+
74
+ ```javascript
75
+ const rule = requireAuth();
76
+ expect(rule(null, {}, { user: { id: '1' } }, null)).toBe(true);
77
+ expect(() => rule(null, {}, {}, null)).toThrow(UnauthenticatedError);
78
+ ```
79
+
80
+ ### Testing validators
81
+
82
+ ```javascript
83
+ const validation = validators.stringLength('Name', 2, 100);
84
+ await expect(validation.CREATE[0].validate('Type', 'field', 'ab', null)).resolves.not.toThrow();
85
+ await expect(validation.CREATE[0].validate('Type', 'field', 'a', null)).rejects.toThrow();
86
+ ```
87
+
88
+ ## Lint After Testing
89
+
90
+ Run `npm run lint` after modifying test files to catch unused imports or style violations.