@simtlix/simfinity-js 2.4.2 → 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.
- package/.cursor/rules/simfinity-architecture.mdc +71 -0
- package/.cursor/rules/simfinity-auth-module.mdc +100 -0
- package/.cursor/rules/simfinity-coding-standards.mdc +62 -0
- package/.cursor/rules/simfinity-core-functions.mdc +82 -0
- package/.cursor/rules/simfinity-documentation.mdc +59 -0
- package/.cursor/rules/simfinity-extensions.mdc +121 -0
- package/.cursor/rules/simfinity-testing.mdc +90 -0
- package/README.md +1428 -1446
- package/package.json +1 -1
- package/src/auth/index.js +6 -6
- package/src/index.js +4 -1
|
@@ -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.
|