@simtlix/simfinity-js 2.0.2 → 2.2.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.
@@ -0,0 +1,250 @@
1
+ import SimfinityError from './errors/simfinity.error.js';
2
+
3
+ /**
4
+ * Creates a validation object that works for both 'save' (CREATE) and 'update' (UPDATE) operations.
5
+ * The validators will be applied to both operations.
6
+ * For CREATE operations, the value must be provided and valid.
7
+ * For UPDATE operations, undefined/null values are allowed (field might not be updated),
8
+ * but if a value is provided, it must be valid.
9
+ */
10
+ const createValidator = (validatorFn, required = false) => {
11
+ // Validator for CREATE operations - value is required if required=true
12
+ const validateCreate = async (typeName, fieldName, value, session) => {
13
+ if (required && (value === null || value === undefined)) {
14
+ throw new SimfinityError(`${fieldName} is required`, 'VALIDATION_ERROR', 400);
15
+ }
16
+ if (value !== null && value !== undefined) {
17
+ await validatorFn(typeName, fieldName, value, session);
18
+ }
19
+ };
20
+
21
+ // Validator for UPDATE operations - value is optional
22
+ const validateUpdate = async (typeName, fieldName, value, session) => {
23
+ // Skip validation if value is not provided (field is not being updated)
24
+ if (value === null || value === undefined) {
25
+ return;
26
+ }
27
+ // If value is provided, validate it
28
+ await validatorFn(typeName, fieldName, value, session);
29
+ };
30
+
31
+ const validatorCreate = { validate: validateCreate };
32
+ const validatorUpdate = { validate: validateUpdate };
33
+
34
+ // Return validations for both CREATE and UPDATE operations
35
+ // Also support 'save'/'update' for backward compatibility (though code uses CREATE/UPDATE)
36
+ return {
37
+ CREATE: [validatorCreate],
38
+ UPDATE: [validatorUpdate],
39
+ save: [validatorCreate], // For backward compatibility
40
+ update: [validatorUpdate], // For backward compatibility
41
+ };
42
+ };
43
+
44
+ /**
45
+ * String validators
46
+ */
47
+ export const stringLength = (name, min, max) => {
48
+ return createValidator(async (typeName, fieldName, value) => {
49
+ if (typeof value !== 'string') {
50
+ throw new SimfinityError(`${name} must be a string`, 'VALIDATION_ERROR', 400);
51
+ }
52
+
53
+ if (min !== undefined && value.length < min) {
54
+ throw new SimfinityError(`${name} must be at least ${min} characters`, 'VALIDATION_ERROR', 400);
55
+ }
56
+
57
+ if (max !== undefined && value.length > max) {
58
+ throw new SimfinityError(`${name} must be at most ${max} characters`, 'VALIDATION_ERROR', 400);
59
+ }
60
+ }, true); // Required for CREATE operations
61
+ };
62
+
63
+ export const maxLength = (name, max) => {
64
+ return createValidator(async (typeName, fieldName, value) => {
65
+ if (typeof value !== 'string') {
66
+ throw new SimfinityError(`${name} must be a string`, 'VALIDATION_ERROR', 400);
67
+ }
68
+
69
+ if (value.length > max) {
70
+ throw new SimfinityError(`${name} must be at most ${max} characters`, 'VALIDATION_ERROR', 400);
71
+ }
72
+ }, false); // Optional
73
+ };
74
+
75
+ export const pattern = (name, regex, message) => {
76
+ const regexObj = typeof regex === 'string' ? new RegExp(regex) : regex;
77
+ const errorMessage = message || `${name} format is invalid`;
78
+
79
+ return createValidator(async (typeName, fieldName, value) => {
80
+ if (typeof value !== 'string') {
81
+ throw new SimfinityError(`${name} must be a string`, 'VALIDATION_ERROR', 400);
82
+ }
83
+
84
+ if (!regexObj.test(value)) {
85
+ throw new SimfinityError(errorMessage, 'VALIDATION_ERROR', 400);
86
+ }
87
+ }, false); // Optional
88
+ };
89
+
90
+ export const email = () => {
91
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
92
+
93
+ return createValidator(async (typeName, fieldName, value) => {
94
+ if (typeof value !== 'string') {
95
+ throw new SimfinityError('Email must be a string', 'VALIDATION_ERROR', 400);
96
+ }
97
+
98
+ if (!emailRegex.test(value)) {
99
+ throw new SimfinityError('Invalid email format', 'VALIDATION_ERROR', 400);
100
+ }
101
+ }, false); // Optional
102
+ };
103
+
104
+ export const url = () => {
105
+ return createValidator(async (typeName, fieldName, value) => {
106
+ if (typeof value !== 'string') {
107
+ throw new SimfinityError('URL must be a string', 'VALIDATION_ERROR', 400);
108
+ }
109
+
110
+ try {
111
+ // Use URL constructor for better validation
112
+ new URL(value);
113
+ } catch (e) {
114
+ console.log('Invalid URL format', e);
115
+ throw new SimfinityError('Invalid URL format', 'VALIDATION_ERROR', 400);
116
+ }
117
+ }, false); // Optional
118
+ };
119
+
120
+ /**
121
+ * Number validators
122
+ */
123
+ export const numberRange = (name, min, max) => {
124
+ return createValidator(async (typeName, fieldName, value) => {
125
+ if (typeof value !== 'number' || isNaN(value)) {
126
+ throw new SimfinityError(`${name} must be a number`, 'VALIDATION_ERROR', 400);
127
+ }
128
+
129
+ if (min !== undefined && value < min) {
130
+ throw new SimfinityError(`${name} must be at least ${min}`, 'VALIDATION_ERROR', 400);
131
+ }
132
+
133
+ if (max !== undefined && value > max) {
134
+ throw new SimfinityError(`${name} must be at most ${max}`, 'VALIDATION_ERROR', 400);
135
+ }
136
+ }, false); // Optional
137
+ };
138
+
139
+ export const positive = (name) => {
140
+ return createValidator(async (typeName, fieldName, value) => {
141
+ if (typeof value !== 'number' || isNaN(value)) {
142
+ throw new SimfinityError(`${name} must be a number`, 'VALIDATION_ERROR', 400);
143
+ }
144
+
145
+ if (value <= 0) {
146
+ throw new SimfinityError(`${name} must be positive`, 'VALIDATION_ERROR', 400);
147
+ }
148
+ }, false); // Optional
149
+ };
150
+
151
+ /**
152
+ * Array validators
153
+ */
154
+ export const arrayLength = (name, maxItems, itemValidator) => {
155
+ return createValidator(async (typeName, fieldName, value, session) => {
156
+ if (!Array.isArray(value)) {
157
+ throw new SimfinityError(`${name} must be an array`, 'VALIDATION_ERROR', 400);
158
+ }
159
+
160
+ if (maxItems !== undefined && value.length > maxItems) {
161
+ throw new SimfinityError(`${name} must have at most ${maxItems} items`, 'VALIDATION_ERROR', 400);
162
+ }
163
+
164
+ // If itemValidator is provided, validate each item
165
+ if (itemValidator && Array.isArray(itemValidator)) {
166
+ for (let i = 0; i < value.length; i++) {
167
+ for (const validator of itemValidator) {
168
+ await validator.validate(typeName, fieldName, value[i], session);
169
+ }
170
+ }
171
+ }
172
+ }, false); // Optional
173
+ };
174
+
175
+ /**
176
+ * Date validators
177
+ */
178
+ export const dateFormat = (name, format) => {
179
+ return createValidator(async (typeName, fieldName, value) => {
180
+ // Handle Date objects, ISO strings, and timestamps
181
+ let date;
182
+ if (value instanceof Date) {
183
+ date = value;
184
+ } else if (typeof value === 'string') {
185
+ date = new Date(value);
186
+ } else if (typeof value === 'number') {
187
+ date = new Date(value);
188
+ } else {
189
+ throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
190
+ }
191
+
192
+ if (isNaN(date.getTime())) {
193
+ throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
194
+ }
195
+
196
+ // If format is provided, validate format
197
+ if (format && typeof value === 'string') {
198
+ // Simple format validation - can be enhanced
199
+ const formatRegex = /^\d{4}-\d{2}-\d{2}$/; // YYYY-MM-DD
200
+ if (format === 'YYYY-MM-DD' && !formatRegex.test(value)) {
201
+ throw new SimfinityError(`${name} must be in format ${format}`, 'VALIDATION_ERROR', 400);
202
+ }
203
+ // Add more format patterns as needed
204
+ }
205
+ }, false); // Optional
206
+ };
207
+
208
+ export const futureDate = (name) => {
209
+ return createValidator(async (typeName, fieldName, value) => {
210
+ let date;
211
+ if (value instanceof Date) {
212
+ date = value;
213
+ } else if (typeof value === 'string') {
214
+ date = new Date(value);
215
+ } else if (typeof value === 'number') {
216
+ date = new Date(value);
217
+ } else {
218
+ throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
219
+ }
220
+
221
+ if (isNaN(date.getTime())) {
222
+ throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
223
+ }
224
+
225
+ if (date <= new Date()) {
226
+ throw new SimfinityError(`${name} must be a future date`, 'VALIDATION_ERROR', 400);
227
+ }
228
+ }, false); // Optional
229
+ };
230
+
231
+ // Export all validators as an object
232
+ const validators = {
233
+ // String validators
234
+ stringLength,
235
+ maxLength,
236
+ pattern,
237
+ email,
238
+ url,
239
+ // Number validators
240
+ numberRange,
241
+ positive,
242
+ // Array validators
243
+ arrayLength,
244
+ // Date validators
245
+ dateFormat,
246
+ futureDate,
247
+ };
248
+
249
+ export default validators;
250
+
@@ -1,10 +0,0 @@
1
- ---
2
- name: Bug report
3
- about: Create a report to help us improve
4
- title: "[BUG]"
5
- labels: bug
6
- assignees: ''
7
-
8
- ---
9
-
10
- **Describe the bug*
@@ -1,10 +0,0 @@
1
- ---
2
- name: Feature request
3
- about: Suggest an idea for this project
4
- title: "[FEATURE]"
5
- labels: feature
6
- assignees: ''
7
-
8
- ---
9
-
10
- ** Describe the feature **
@@ -1,19 +0,0 @@
1
- name: Build on master
2
-
3
- on:
4
- push:
5
- branches: master
6
-
7
- jobs:
8
- build:
9
- runs-on: ubuntu-24.04
10
- steps:
11
- - uses: actions/checkout@v2.3.4
12
- - uses: actions/setup-node@v1.4.4
13
- with:
14
- node-version: 14
15
- - run: npm ci
16
- - run: npx json -f package.json peerDependencies | npx json -ka | xargs -i{} bash -c 'echo $0@$(npx json -f package.json peerDependencies.$0)' {} | xargs -i{} npm install --save-optional {}
17
- - run: npm run lint
18
- - run: npm test
19
-
@@ -1,45 +0,0 @@
1
- name: Publish packages
2
-
3
- on:
4
- create:
5
- tags:
6
- - 'v*'
7
-
8
- jobs:
9
- build:
10
- runs-on: ubuntu-24.04
11
- steps:
12
- - uses: actions/checkout@v2.3.4
13
- - uses: actions/setup-node@v1.4.4
14
- with:
15
- node-version: 14
16
- - run: npm ci
17
- - run: npx json -f package.json peerDependencies | npx json -ka | xargs -i{} bash -c 'echo $0@$(npx json -f package.json peerDependencies.$0)' {} | xargs -i{} npm install --save-optional {}
18
- - run: npm test
19
-
20
- publish-npm:
21
- needs: build
22
- runs-on: ubuntu-24.04
23
- steps:
24
- - uses: actions/checkout@v2.3.4
25
- - uses: actions/setup-node@v1.4.4
26
- with:
27
- node-version: 14
28
- registry-url: https://registry.npmjs.org/
29
- - run: npm ci
30
- - run: npm publish --access public
31
- env:
32
- NODE_AUTH_TOKEN: ${{secrets.NPMJS_TOKEN}}
33
- publish-gpr:
34
- needs: build
35
- runs-on: ubuntu-24.04
36
- steps:
37
- - uses: actions/checkout@v2.3.4
38
- - uses: actions/setup-node@v1.4.4
39
- with:
40
- node-version: 14
41
- registry-url: https://npm.pkg.github.com/
42
- - run: npm ci
43
- - run: npm publish
44
- env:
45
- NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -1,65 +0,0 @@
1
- name: Generate Release
2
-
3
- on:
4
- push:
5
- branches:
6
- - release/*
7
-
8
- jobs:
9
- generate_release:
10
- runs-on: ubuntu-24.04
11
- steps:
12
- - name: Inject slug/short variables
13
- uses: rlespinasse/github-slug-action@3.1.0
14
- - uses: actions/checkout@v2.3.4
15
- - uses: actions/setup-node@v1.4.4
16
- with:
17
- node-version: 14
18
- - run: npm ci
19
- - run: npx json -f package.json peerDependencies | npx json -ka | xargs -i{} bash -c 'echo $0@$(npx json -f package.json peerDependencies.$0)' {} | xargs -i{} npm install --save-optional {}
20
- - run: npm run lint
21
- - run: npm test
22
- - run: npm version --no-git-tag-version $(echo ${{ env.GITHUB_REF_SLUG }} | cut -d"-" -f 2)
23
- - name: Set release version
24
- uses: stefanzweifel/git-auto-commit-action@v4
25
- with:
26
- commit_message: "Set Release version to ${{ env.GITHUB_REF_SLUG }}"
27
- - name: Push changes to master
28
- uses: ad-m/github-push-action@master
29
- with:
30
- github_token: ${{ secrets.GITHUB_TOKEN }}
31
- branch: ${{ env.GITHUB_REF_SLUG }}
32
- - name: Create tag
33
- uses: Klemensas/action-autotag@stable
34
- id: update_tag
35
- with:
36
- GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
37
- tag_prefix: "v"
38
- - name: Create Release
39
- if: steps.update_tag.outputs.tagname
40
- id: create_release
41
- uses: actions/create-release@v1
42
- env:
43
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
- with:
45
- tag_name: ${{ steps.update_tag.outputs.tagname }}
46
- release_name: Release ${{ steps.update_tag.outputs.tagname }}
47
- body: |
48
- Changes in this Release
49
- draft: false
50
- prerelease: false
51
-
52
- - name: "Set next development version"
53
- uses: actions/setup-node@v1.4.4
54
- with:
55
- node-version: 14
56
- - run: npm version --no-git-tag-version preminor --preid alpha
57
- - name: "Push next development version to master"
58
- uses: stefanzweifel/git-auto-commit-action@v4
59
- with:
60
- commit_message: "Set next development version"
61
- - name: Push changes
62
- uses: ad-m/github-push-action@master
63
- with:
64
- github_token: ${{ secrets.GITHUB_TOKEN }}
65
- branch: master
package/BACKUP_README.md DELETED
@@ -1,26 +0,0 @@
1
- [![npm version](https://badge.fury.io/js/%40simtlix%2Fsimfinity-js.svg)](https://badge.fury.io/js/%40simtlix%2Fsimfinity-js)
2
-
3
- # How to
4
- ## Install
5
- ```bash
6
- npm install @simtlix/simfinity-js --save
7
- ```
8
-
9
- To use this lib:
10
- * Define your mongoose models
11
- * Define your GraphQL types
12
- * Register models and types using `connect` function for **non embedded** types and `addNoEndpointType` function for **embedded** ones
13
- * Create the GraphQL schema using `createSchema` function
14
-
15
- ## Test
16
- On this project root directory
17
- `npm link`
18
-
19
- On the test project root directory
20
- `npm link @simtlix/simfinity-js`
21
-
22
- Run test project with *preserve-symlinks* flag. E.g.:
23
- `node --preserve-symlinks app.js`
24
-
25
- # Example
26
- There is a sample of an app using this lib at [simfinity.js-samples](https://github.com/simtlix/simfinity.js-samples)
@@ -1,252 +0,0 @@
1
- # Automatic ObjectId Index Creation
2
-
3
- This feature automatically creates MongoDB indexes for all ObjectId fields when models are generated, including nested embedded objects.
4
-
5
- ## How it works
6
-
7
- When you define GraphQL types with ObjectId fields (GraphQLID), the system will automatically:
8
-
9
- 1. Analyze the schema definition to find all ObjectId fields
10
- 2. Create indexes for each ObjectId field found
11
- 3. Handle nested embedded objects recursively
12
- 4. Create indexes for arrays of embedded objects
13
- 5. Create indexes for relationship fields (foreign keys between types)
14
-
15
- ## Examples
16
-
17
- ### Simple ObjectId fields
18
-
19
- ```javascript
20
- import { GraphQLObjectType, GraphQLString, GraphQLID } from 'graphql';
21
- import * as simfinity from '@simtlix/simfinity-js';
22
-
23
- const UserType = new GraphQLObjectType({
24
- name: 'User',
25
- fields: () => ({
26
- id: { type: GraphQLID },
27
- name: { type: GraphQLString },
28
- email: { type: GraphQLString },
29
- managerId: { type: GraphQLID }, // This will get an index
30
- }),
31
- });
32
-
33
- simfinity.connect(null, UserType, 'user', 'users');
34
- simfinity.createSchema();
35
- ```
36
-
37
- This will create indexes for:
38
- - `id` field
39
- - `managerId` field
40
-
41
- ### Relationship fields (non-embedded)
42
-
43
- ```javascript
44
- const DepartmentType = new GraphQLObjectType({
45
- name: 'Department',
46
- fields: () => ({
47
- id: { type: GraphQLID },
48
- name: { type: GraphQLString },
49
- }),
50
- });
51
-
52
- const UserType = new GraphQLObjectType({
53
- name: 'User',
54
- fields: () => ({
55
- id: { type: GraphQLID },
56
- name: { type: GraphQLString },
57
- email: { type: GraphQLString },
58
- department: {
59
- type: DepartmentType,
60
- extensions: {
61
- relation: { embedded: false } // or omit this line (default is false)
62
- }
63
- },
64
- }),
65
- });
66
-
67
- // Register both types
68
- simfinity.connect(null, DepartmentType, 'department', 'departments');
69
- simfinity.connect(null, UserType, 'user', 'users');
70
- simfinity.createSchema();
71
- ```
72
-
73
- This will create indexes for:
74
- - `id` field (in both User and Department)
75
- - `department` field (the foreign key field that references Department)
76
-
77
- ### One-to-Many relationships
78
-
79
- ```javascript
80
- const OrderType = new GraphQLObjectType({
81
- name: 'Order',
82
- fields: () => ({
83
- id: { type: GraphQLID },
84
- orderNumber: { type: GraphQLString },
85
- customerId: { type: GraphQLID }, // This will get an index
86
- items: {
87
- type: new GraphQLList(OrderItemType),
88
- extensions: {
89
- relation: { embedded: false }
90
- }
91
- },
92
- }),
93
- });
94
-
95
- const OrderItemType = new GraphQLObjectType({
96
- name: 'OrderItem',
97
- fields: () => ({
98
- id: { type: GraphQLID },
99
- productId: { type: GraphQLID }, // This will get an index
100
- quantity: { type: GraphQLString },
101
- orderId: { type: GraphQLID }, // This will get an index (foreign key)
102
- }),
103
- });
104
-
105
- // Register both types
106
- simfinity.connect(null, OrderItemType, 'orderItem', 'orderItems');
107
- simfinity.connect(null, OrderType, 'order', 'orders');
108
- simfinity.createSchema();
109
- ```
110
-
111
- This will create indexes for:
112
- - `id` field (in both Order and OrderItem)
113
- - `customerId` field (in Order)
114
- - `productId` field (in OrderItem)
115
- - `orderId` field (in OrderItem - the foreign key that references Order)
116
-
117
- ### Embedded objects with ObjectId
118
-
119
- ```javascript
120
- const AddressType = new GraphQLObjectType({
121
- name: 'Address',
122
- fields: () => ({
123
- street: { type: GraphQLString },
124
- city: { type: GraphQLString },
125
- countryId: { type: GraphQLID }, // This will get an index
126
- }),
127
- });
128
-
129
- const UserType = new GraphQLObjectType({
130
- name: 'User',
131
- fields: () => ({
132
- id: { type: GraphQLID },
133
- name: { type: GraphQLString },
134
- address: {
135
- type: AddressType,
136
- extensions: {
137
- relation: { embedded: true }
138
- }
139
- },
140
- }),
141
- });
142
-
143
- // Register the embedded type
144
- simfinity.addNoEndpointType(AddressType);
145
- simfinity.connect(null, UserType, 'user', 'users');
146
- simfinity.createSchema();
147
- ```
148
-
149
- This will create indexes for:
150
- - `id` field
151
- - `countryId` field (from AddressType)
152
- - `address.countryId` field (embedded path)
153
-
154
- ### Nested embedded objects
155
-
156
- ```javascript
157
- const CityType = new GraphQLObjectType({
158
- name: 'City',
159
- fields: () => ({
160
- name: { type: GraphQLString },
161
- stateId: { type: GraphQLID }, // This will get an index
162
- }),
163
- });
164
-
165
- const AddressType = new GraphQLObjectType({
166
- name: 'Address',
167
- fields: () => ({
168
- street: { type: GraphQLString },
169
- city: {
170
- type: CityType,
171
- extensions: {
172
- relation: { embedded: true }
173
- }
174
- },
175
- }),
176
- });
177
-
178
- const UserType = new GraphQLObjectType({
179
- name: 'User',
180
- fields: () => ({
181
- id: { type: GraphQLID },
182
- name: { type: GraphQLString },
183
- address: {
184
- type: AddressType,
185
- extensions: {
186
- relation: { embedded: true }
187
- }
188
- },
189
- }),
190
- });
191
-
192
- // Register the embedded types
193
- simfinity.addNoEndpointType(CityType);
194
- simfinity.addNoEndpointType(AddressType);
195
- simfinity.connect(null, UserType, 'user', 'users');
196
- simfinity.createSchema();
197
- ```
198
-
199
- This will create indexes for:
200
- - `id` field
201
- - `stateId` field (from CityType)
202
- - `city.stateId` field (from AddressType)
203
- - `address.city.stateId` field (nested embedded path)
204
-
205
- ### Arrays of embedded objects
206
-
207
- ```javascript
208
- const OrderItemType = new GraphQLObjectType({
209
- name: 'OrderItem',
210
- fields: () => ({
211
- productId: { type: GraphQLID }, // This will get an index
212
- quantity: { type: GraphQLString },
213
- }),
214
- });
215
-
216
- const OrderType = new GraphQLObjectType({
217
- name: 'Order',
218
- fields: () => ({
219
- id: { type: GraphQLID },
220
- customerId: { type: GraphQLID },
221
- items: {
222
- type: new GraphQLList(OrderItemType),
223
- extensions: {
224
- relation: { embedded: true }
225
- }
226
- },
227
- }),
228
- });
229
-
230
- // Register the embedded type
231
- simfinity.addNoEndpointType(OrderItemType);
232
- simfinity.connect(null, OrderType, 'order', 'orders');
233
- simfinity.createSchema();
234
- ```
235
-
236
- This will create indexes for:
237
- - `id` field
238
- - `customerId` field
239
- - `productId` field (from OrderItemType)
240
- - `items.productId` field (array embedded path)
241
-
242
- ## Benefits
243
-
244
- 1. **Automatic Performance Optimization**: ObjectId fields are automatically indexed for better query performance
245
- 2. **Relationship Optimization**: Foreign key fields are automatically indexed for efficient joins and lookups
246
- 3. **No Manual Configuration**: No need to manually specify which fields should be indexed
247
- 4. **Handles Complex Schemas**: Works with nested embedded objects and arrays
248
- 5. **Consistent**: All ObjectId fields get the same treatment regardless of their location in the schema
249
-
250
- ## Technical Details
251
-
252
- The system uses the `mongoose.Schema` constructor to create schemas and then calls `schema.index()` for each ObjectId field found. The index creation happens during model generation, so it's transparent to the application code.
Binary file