@ealforque/sequelize-field-parser 1.0.4 → 1.0.6

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  ![npm version](https://img.shields.io/npm/v/@ealforque/sequelize-field-parser)
4
4
  ![build](https://github.com/ealforque/sequelize-field-parser/actions/workflows/release.yaml/badge.svg)
5
+ ![license](https://img.shields.io/badge/license-MIT-green)
6
+ [![Socket Badge](https://badge.socket.dev/npm/package/@ealforque/sequelize-field-parser)](https://badge.socket.dev/npm/package/@ealforque/sequelize-field-parser)
5
7
 
6
8
  ## Description
7
9
 
@@ -9,11 +11,17 @@ A TypeScript utility for Sequelize models that lets users specify fields to incl
9
11
 
10
12
  ## Features
11
13
 
12
- - Parse Sequelize model fields and relationships
13
- - Generate field trees for complex models
14
- - Type-safe interfaces and types
15
- - Easy integration with MySQL via Sequelize
16
- - Test-driven development with Jest
14
+ - **Parse Sequelize model fields and relationships:** Easily extract and validate fields and associations from models
15
+ - **Generate field trees for complex models:** Build nested relationship trees for Sequelize includes
16
+ - **Type-safe interfaces and types:** All parsing and tree generation is type-safe
17
+ - **Easy integration with MySQL via Sequelize:** Works seamlessly with Sequelize ORM
18
+ - **Test-driven development with Jest:** Comprehensive test suite for robust behavior
19
+ - **Handles maximum relationship depth (default: 10):** Prevents runaway includes and logs warnings
20
+ - **Detects and prevents circular relationships:** Safely handles circular model associations
21
+ - **Handles malformed input:** Catches empty, whitespace, consecutive dots, leading/trailing dots
22
+ - **Deduplicates columns:** Duplicate fields in the input string will not result in duplicate entries in the `columns` array
23
+ - **Model requirements:** Models must define static `DEFAULT_FIELDS` and `SELECTABLE_FIELDS` properties. If these are missing, no fields will be selectable and all user-specified fields may be reported as invalid.
24
+ - **Input requirements:** Only valid Sequelize model classes or objects with the required static properties should be passed. Passing a non-model or incorrectly typed object may result in type errors or runtime errors.
17
25
 
18
26
  ## Installation
19
27
 
@@ -21,42 +29,212 @@ A TypeScript utility for Sequelize models that lets users specify fields to incl
21
29
  npm install @ealforque/sequelize-field-parser
22
30
  ```
23
31
 
24
- ## Usage
32
+ ## Usage (Correct Example)
25
33
 
26
- Import and use in your project:
34
+ ```typescript
35
+ import FieldParserService from "./src/field_parser.service";
36
+ import SomeModel from "./models/SomeModel";
37
+
38
+ const parser = new FieldParserService();
39
+ const result = parser.parseFields("id,name,profile.email", SomeModel);
40
+ console.log(result);
41
+ /*
42
+ {
43
+ columns: ['id', 'name'],
44
+ relationshipTree: { profile: { email: true } },
45
+ invalidFields: []
46
+ }
47
+ */
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Edge Case Handling
53
+
54
+ ### Malformed Input
55
+
56
+ ```typescript
57
+ const parser = new FieldParserService();
58
+ const result = parser.parseFields(" ,foo..bar,.baz,", SomeModel);
59
+ console.log(result); // invalidFields will include malformed entries
60
+ /*
61
+ {
62
+ columns: [],
63
+ relationshipTree: {},
64
+ invalidFields: ['foo..bar', '.baz']
65
+ }
66
+ */
67
+ ```
68
+
69
+ ### Missing Static Properties
70
+
71
+ ```typescript
72
+ const parser = new FieldParserService();
73
+ const result = parser.parseFields("id,name", ModelWithoutStatics);
74
+ console.log(result); // columns will be empty, invalidFields will include all
75
+ /*
76
+ {
77
+ columns: [],
78
+ relationshipTree: {},
79
+ invalidFields: ['id', 'name']
80
+ }
81
+ */
82
+ ```
83
+
84
+ ### Non-Model Input
85
+
86
+ ```typescript
87
+ const parser = new FieldParserService();
88
+ const result = parser.parseFields("id,name", {});
89
+ console.log(result); // columns empty, invalidFields includes all
90
+ /*
91
+ {
92
+ columns: [],
93
+ relationshipTree: {},
94
+ invalidFields: ['id', 'name']
95
+ }
96
+ */
97
+ ```
98
+
99
+ ### Non-Existent Associations
100
+
101
+ ```typescript
102
+ const parser = new FieldParserService();
103
+ const result = parser.parseFields("profile.address.zip", SomeModel);
104
+ console.log(result); // invalidFields includes non-existent associations
105
+ /*
106
+ {
107
+ columns: [],
108
+ relationshipTree: {},
109
+ invalidFields: ['profile.address.zip']
110
+ }
111
+ */
112
+ ```
113
+
114
+ ### Deeply Nested/Circular Relationships
27
115
 
28
116
  ```typescript
29
- import FieldParserService from "sequelize-field-parser";
30
- import Status from "./path/to/status.model";
117
+ const parser = new FieldParserService();
118
+ const result = parser.parseFields("a.b.c.d.e.f.g.h.i.j.k", SomeModel);
119
+ console.log(result); // invalidFields includes overly deep/circular fields
120
+ /*
121
+ {
122
+ columns: [],
123
+ relationshipTree: {},
124
+ invalidFields: ['a.b.c.d.e.f.g.h.i.j.k']
125
+ }
126
+ */
127
+ ```
31
128
 
129
+ ### Duplicate Fields
130
+
131
+ ```typescript
32
132
  const parser = new FieldParserService();
133
+ const result = parser.parseFields("id,id,name,name", SomeModel);
134
+ console.log(result); // columns deduplicated
135
+ /*
136
+ {
137
+ columns: ['id', 'name'],
138
+ relationshipTree: {},
139
+ invalidFields: []
140
+ }
141
+ */
142
+ ```
33
143
 
34
- // query parameter
35
- // api/resource?fields='status.uuid,status.name,status.category.uuid,status.category.name'
36
- const queryParams =
37
- "status.uuid,status.name,status.category.uuid,status.category.name";
144
+ ### Relationship Leaf Attribute Filtering
38
145
 
39
- // Parse the query parameter
40
- const { columns, relationshipTree } = parser.parseFields(queryParams, Model);
146
+ If a nested relationship contains attributes not in `SELECTABLE_FIELDS`, those attributes are filtered out and a warning is logged for each ignored attribute.
147
+
148
+ ```typescript
149
+ const parser = new FieldParserService();
150
+ const tree = {
151
+ status: {
152
+ name: true,
153
+ invalid_field: true,
154
+ },
155
+ };
156
+ const mockModel = {
157
+ associations: {
158
+ status: {
159
+ target: { SELECTABLE_FIELDS: ["name"] },
160
+ },
161
+ },
162
+ };
163
+ const result = parser.buildSequelizeInclude(tree, mockModel);
164
+ console.log(result);
165
+ /*
166
+ Warning: FieldParserService: Attribute 'invalid_field' in relationship 'status' is not in SELECTABLE_FIELDS and will be ignored.
167
+ [
168
+ {
169
+ model: { SELECTABLE_FIELDS: ["name"] },
170
+ as: "status",
171
+ attributes: ["name"],
172
+ include: [],
173
+ },
174
+ ]
175
+ */
176
+ ```
41
177
 
42
- // Build the sequelize include
43
- const include = parser.buildSequelizeInclude(relationshipTree, Model);
178
+ ### Empty Relationship Attributes (No DEFAULT_FIELDS)
44
179
 
45
- console.log(include);
46
- /* Example output:
180
+ If a relationship is specified but no attributes are selected and the related model does not define `DEFAULT_FIELDS`, a warning is logged and the attributes array will be empty.
181
+
182
+ ```typescript
183
+ const parser = new FieldParserService();
184
+ const tree = {
185
+ status: {}, // No attributes selected
186
+ };
187
+ const mockModel = {
188
+ associations: {
189
+ status: {
190
+ target: { SELECTABLE_FIELDS: [] }, // No DEFAULT_FIELDS
191
+ },
192
+ },
193
+ };
194
+ const result = parser.buildSequelizeInclude(tree, mockModel);
195
+ console.log(result);
196
+ /*
197
+ Warning: FieldParserService: No attributes selected and no DEFAULT_FIELDS available for relationship 'status'. Attributes array will be empty.
47
198
  [
48
199
  {
49
- model: Status,
50
- as: 'status',
51
- attributes: ['uuid', 'name'],
52
- include: [
53
- {
54
- model: Category,
55
- as: 'category',
56
- attributes: ['uuid', 'name'],
57
- }
58
- ]
59
- }
200
+ model: { SELECTABLE_FIELDS: [] },
201
+ as: "status",
202
+ attributes: [],
203
+ include: [],
204
+ },
60
205
  ]
61
206
  */
62
207
  ```
208
+
209
+ ### Association Alias Mismatches
210
+
211
+ If the association alias in the model does not match the field string, the relationship will not be included and a warning is logged.
212
+
213
+ ```typescript
214
+ const parser = new FieldParserService();
215
+ const mockModel = {
216
+ associations: {}, // No 'profile' association
217
+ SELECTABLE_FIELDS: ["id", "name"],
218
+ };
219
+ const result = parser.parseFields("profile.email", mockModel);
220
+ console.log(result);
221
+ /*
222
+ Warning: FieldParserService: Association alias 'profile' does not exist in model 'unknown'. Field 'profile.email' will be treated as invalid.
223
+ {
224
+ columns: [],
225
+ relationshipTree: {},
226
+ invalidFields: ["profile.email"]
227
+ }
228
+ */
229
+ ```
230
+
231
+ ## Supply Chain Security
232
+
233
+ This package runs `npm audit` in its CI workflow to check for vulnerabilities in dependencies before publishing. Automated dependency updates and vulnerability checks are enabled for maximum supply chain security.
234
+
235
+ Example GitHub Actions step:
236
+
237
+ ```yaml
238
+ - name: Audit dependencies
239
+ run: npm audit --audit-level=high
240
+ ```
@@ -7,7 +7,8 @@ declare class FieldParserService {
7
7
  parseFields: <M extends Model>(fields: string, model: ModelStaticWithFields<M>) => {
8
8
  columns: string[];
9
9
  relationshipTree: RelationshipTree;
10
+ invalidFields: string[];
10
11
  };
11
- buildSequelizeInclude: <M extends Model>(tree: RelationshipTree, model: ModelStatic<M>) => IncludeOptions[];
12
+ buildSequelizeInclude: <M extends Model>(tree: RelationshipTree, model: ModelStatic<M>, depth?: number, visitedModels?: Set<any>) => IncludeOptions[];
12
13
  }
13
14
  export default FieldParserService;
@@ -6,37 +6,100 @@ class FieldParserService {
6
6
  this.fields = [];
7
7
  }
8
8
  parseFields = (fields, model) => {
9
+ if (typeof model !== "object" ||
10
+ model === null ||
11
+ !("associations" in model) ||
12
+ typeof model.associations !== "object") {
13
+ console.warn("FieldParserService: Input is not a valid Sequelize model. Returning all fields as invalid.");
14
+ const inputFields = fields
15
+ .split(",")
16
+ .map((f) => f.trim())
17
+ .filter((f) => f.length > 0);
18
+ return {
19
+ columns: [],
20
+ relationshipTree: {},
21
+ invalidFields: inputFields,
22
+ };
23
+ }
9
24
  this.fields = fields
10
25
  .split(",")
11
26
  .map((f) => f.trim())
12
27
  .filter((f) => f.length > 0);
13
28
  const relationshipTree = {};
14
- const columns = [...model.DEFAULT_FIELDS];
15
- const selectableFields = model.SELECTABLE_FIELDS;
29
+ const invalidFields = [];
30
+ const safeModel = model;
31
+ const columns = Array.isArray(safeModel.DEFAULT_FIELDS)
32
+ ? [...safeModel.DEFAULT_FIELDS]
33
+ : [];
34
+ const selectableFields = Array.isArray(safeModel.SELECTABLE_FIELDS)
35
+ ? safeModel.SELECTABLE_FIELDS
36
+ : [];
16
37
  for (const field of this.fields) {
38
+ // Basic validation to catch empty fields, fields with only whitespace, and invalid dot usage
39
+ if (field === "" ||
40
+ /^\s*$/.test(field) ||
41
+ /\.\./.test(field) ||
42
+ /^\.|\.$/.test(field)) {
43
+ invalidFields.push(field);
44
+ continue;
45
+ }
17
46
  const segments = field.split(".");
18
47
  if (segments.length === 1) {
19
48
  const topField = segments[0];
20
49
  if (selectableFields.includes(topField)) {
21
50
  columns.push(topField);
22
51
  }
52
+ else {
53
+ invalidFields.push(field);
54
+ }
23
55
  }
24
56
  else {
25
57
  let relationship = relationshipTree;
58
+ let currentModel = model;
59
+ let valid = true;
26
60
  for (let i = 0; i < segments.length; i++) {
27
- const field = segments[i];
28
- if (!relationship[field]) {
29
- relationship[field] = i === segments.length - 1 ? true : {};
61
+ const seg = segments[i];
62
+ if (i < segments.length - 1) {
63
+ if (!currentModel.associations || !currentModel.associations[seg]) {
64
+ console.warn(`FieldParserService: Association alias '${seg}' does not exist in model '${currentModel.name || "unknown"}'. Field '${field}' will be treated as invalid.`);
65
+ valid = false;
66
+ break;
67
+ }
68
+ currentModel = currentModel.associations[seg].target;
69
+ if (!relationship[seg]) {
70
+ relationship[seg] = {};
71
+ }
72
+ relationship = relationship[seg];
30
73
  }
31
- if (relationship[field] !== true) {
32
- relationship = relationship[field];
74
+ else {
75
+ if (currentModel.SELECTABLE_FIELDS &&
76
+ currentModel.SELECTABLE_FIELDS.includes(seg)) {
77
+ relationship[seg] = true;
78
+ }
79
+ else {
80
+ valid = false;
81
+ }
33
82
  }
34
83
  }
84
+ if (!valid) {
85
+ invalidFields.push(field);
86
+ }
35
87
  }
36
88
  }
37
- return { columns, relationshipTree };
89
+ const uniqueColumns = Array.from(new Set(columns));
90
+ return { columns: uniqueColumns, relationshipTree, invalidFields };
38
91
  };
39
- buildSequelizeInclude = (tree, model) => {
92
+ buildSequelizeInclude = (tree, model, depth = 0, visitedModels = new Set()) => {
93
+ const MAX_DEPTH = 10;
94
+ if (depth > MAX_DEPTH) {
95
+ console.warn("Maximum include depth exceeded.");
96
+ return [];
97
+ }
98
+ if (visitedModels.has(model)) {
99
+ console.warn("Circular relationship detected.");
100
+ return [];
101
+ }
102
+ visitedModels.add(model);
40
103
  const includeOptions = Object.entries(tree).flatMap(([relation, nested]) => {
41
104
  const relationship = model.associations?.[relation];
42
105
  if (!relationship || !relationship.target)
@@ -50,18 +113,34 @@ class FieldParserService {
50
113
  attributes: [],
51
114
  };
52
115
  }
53
- const deeperIncludes = this.buildSequelizeInclude(nested, currentModel);
116
+ const deeperIncludes = this.buildSequelizeInclude(nested, currentModel, depth + 1, visitedModels);
54
117
  const leafAttributes = Object.entries(nested)
55
118
  .filter(([, value]) => value === true)
56
- .map(([key]) => key)
57
- .filter((attr) => selectableFields.includes(attr));
119
+ .map(([key]) => key);
120
+ const filteredLeafAttributes = leafAttributes.filter((attr) => {
121
+ const isSelectable = selectableFields.includes(attr);
122
+ if (!isSelectable) {
123
+ console.warn(`FieldParserService: Attribute '${attr}' in relationship '${relation}' is not in SELECTABLE_FIELDS and will be ignored.`);
124
+ }
125
+ return isSelectable;
126
+ });
127
+ let attributesToUse = filteredLeafAttributes;
128
+ if (attributesToUse.length === 0) {
129
+ if (Array.isArray(currentModel.DEFAULT_FIELDS)) {
130
+ attributesToUse = [...currentModel.DEFAULT_FIELDS];
131
+ }
132
+ else {
133
+ console.warn(`FieldParserService: No attributes selected and no DEFAULT_FIELDS available for relationship '${relation}'. Attributes array will be empty.`);
134
+ }
135
+ }
58
136
  return {
59
137
  model: currentModel,
60
138
  as: relation,
61
- attributes: leafAttributes,
139
+ attributes: attributesToUse,
62
140
  include: deeperIncludes,
63
141
  };
64
142
  });
143
+ visitedModels.delete(model);
65
144
  return includeOptions;
66
145
  };
67
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ealforque/sequelize-field-parser",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "main": "dist/field_parser.service.js",
5
5
  "types": "dist/field_parser.service.d.ts",
6
6
  "files": [
@@ -17,22 +17,23 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "sequelize": "^6.0.0"
20
+ "sequelize": "6.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/jest": "^29.5.14",
24
- "eslint": "^9.26.0",
25
- "eslint-config-prettier": "^10.1.3",
26
- "eslint-plugin-import": "^2.31.0",
27
- "eslint-plugin-simple-import-sort": "^12.1.1",
28
- "http-status-codes": "^2.3.0",
29
- "jest": "^29.7.0",
30
- "prettier": "^3.5.3",
31
- "sequelize-cli": "^6.6.2",
32
- "supertest": "^7.1.0",
33
- "ts-jest": "^29.3.2",
34
- "ts-node": "^10.9.2",
35
- "typescript": "^5.8.3",
36
- "typescript-eslint": "^8.32.0"
37
- }
23
+ "@types/jest": "29.5.14",
24
+ "eslint": "9.26.0",
25
+ "eslint-config-prettier": "10.1.3",
26
+ "eslint-plugin-import": "2.31.0",
27
+ "eslint-plugin-simple-import-sort": "12.1.1",
28
+ "http-status-codes": "2.3.0",
29
+ "jest": "29.7.0",
30
+ "prettier": "3.5.3",
31
+ "sequelize-cli": "6.6.2",
32
+ "supertest": "7.1.0",
33
+ "ts-jest": "29.3.2",
34
+ "ts-node": "10.9.2",
35
+ "typescript": "5.8.3",
36
+ "typescript-eslint": "8.32.0"
37
+ },
38
+ "license": "MIT"
38
39
  }