json-rest-api 1.0.0 → 1.0.1
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/.claude/settings.local.json +16 -0
- package/LICENSE +674 -0
- package/README.md +655 -309
- package/RELATIONSHIPS.md +1013 -0
- package/TODO_rest-api-plugin-extras.js +142 -0
- package/connectors_ideas/express-plugin.js +120 -0
- package/connectors_ideas/fastify.js +219 -0
- package/connectors_ideas/http-plugin.js +138 -0
- package/connectors_ideas/koa-plugin.js +113 -0
- package/docs/fileUploads.md +722 -0
- package/docs/mini-guides/file-uploads-complete.md +159 -0
- package/examples/.gitkeep +0 -0
- package/examples/express-example.js +262 -0
- package/examples/express-plugin.js +366 -0
- package/examples/file-upload-example.js +205 -0
- package/examples/filename-strategies.js +140 -0
- package/examples/knex-basic.js +66 -0
- package/index.js +31 -56
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +41 -14
- package/payloadValdators.js +686 -0
- package/plugins/core/connectors/express-plugin.js +449 -0
- package/plugins/core/connectors/http-plugin.js +450 -0
- package/plugins/core/file-handling-plugin.js +338 -0
- package/plugins/core/lib/busboy-detector.js +167 -0
- package/plugins/core/lib/formidable-detector.js +130 -0
- package/plugins/core/rest-api-knex-plugin.js +200 -0
- package/plugins/core/rest-api-plugin.js +1419 -0
- package/plugins/storage/local-storage.js +218 -0
- package/plugins/storage/s3-storage.js +125 -0
- package/test-examples/package-lock.json +906 -0
- package/test-examples/package.json +15 -0
- package/test-examples/test-basic-api.js +11 -0
- package/test-examples/test-books-resource.js +116 -0
- package/test-examples/test-complete-file-upload.js +140 -0
- package/test-examples/test-debug.js +28 -0
- package/test-examples/test-express-plugin.js +177 -0
- package/test-examples/test-express-simple.js +28 -0
- package/test-examples/test-express-with-import.js +71 -0
- package/test-examples/test-file-upload.js +132 -0
- package/test-examples/test-http-plugin.js +159 -0
- package/test-examples/test-programmatic-api.js +188 -0
- package/test-examples/test-rest-api-plugin.js +13 -0
- package/TODO.todo +0 -113
- package/api.js +0 -628
- package/docs/API.md +0 -1306
- package/docs/FIXES_APPLIED.md +0 -59
- package/docs/GUIDES.md +0 -1792
- package/docs/RESOURCES_IN_EXPRESS.md +0 -456
- package/docs/example-versioning.js +0 -168
- package/docs/example.js +0 -195
- package/plugins/API guide.md +0 -364
- package/plugins/api-registry.js +0 -142
- package/plugins/api-versioning.md +0 -135
- package/plugins/http.js +0 -273
- package/plugins/logging.js +0 -255
- package/plugins/memory.js +0 -182
- package/plugins/mysql.js +0 -753
- package/plugins/openapi.js +0 -401
- package/plugins/positioning.js +0 -220
- package/plugins/security.js +0 -261
- package/plugins/validation.js +0 -173
- package/plugins/versioning.js +0 -288
- package/resource-helper.js +0 -79
- package/schema.js +0 -481
- package/test-basic.js +0 -107
package/plugins/openapi.js
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAPI (Swagger) documentation plugin
|
|
3
|
-
*/
|
|
4
|
-
export const OpenAPIPlugin = {
|
|
5
|
-
install(api, options = {}) {
|
|
6
|
-
const defaultOptions = {
|
|
7
|
-
title: 'API Documentation',
|
|
8
|
-
version: '1.0.0',
|
|
9
|
-
description: 'REST API with JSON:API specification',
|
|
10
|
-
servers: [{ url: 'http://localhost:3000/api' }],
|
|
11
|
-
contact: {},
|
|
12
|
-
license: {},
|
|
13
|
-
...options
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Generate OpenAPI specification
|
|
17
|
-
api.generateOpenAPISpec = () => {
|
|
18
|
-
const spec = {
|
|
19
|
-
openapi: '3.0.0',
|
|
20
|
-
info: {
|
|
21
|
-
title: defaultOptions.title,
|
|
22
|
-
version: defaultOptions.version,
|
|
23
|
-
description: defaultOptions.description,
|
|
24
|
-
contact: defaultOptions.contact,
|
|
25
|
-
license: defaultOptions.license
|
|
26
|
-
},
|
|
27
|
-
servers: defaultOptions.servers,
|
|
28
|
-
paths: {},
|
|
29
|
-
components: {
|
|
30
|
-
schemas: {},
|
|
31
|
-
securitySchemes: {
|
|
32
|
-
bearerAuth: {
|
|
33
|
-
type: 'http',
|
|
34
|
-
scheme: 'bearer',
|
|
35
|
-
bearerFormat: 'JWT'
|
|
36
|
-
},
|
|
37
|
-
apiKey: {
|
|
38
|
-
type: 'apiKey',
|
|
39
|
-
in: 'header',
|
|
40
|
-
name: 'X-API-Key'
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
parameters: {
|
|
44
|
-
idParam: {
|
|
45
|
-
name: 'id',
|
|
46
|
-
in: 'path',
|
|
47
|
-
required: true,
|
|
48
|
-
schema: { type: 'string' },
|
|
49
|
-
description: 'Resource identifier'
|
|
50
|
-
},
|
|
51
|
-
typeParam: {
|
|
52
|
-
name: 'type',
|
|
53
|
-
in: 'path',
|
|
54
|
-
required: true,
|
|
55
|
-
schema: { type: 'string' },
|
|
56
|
-
description: 'Resource type'
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
responses: {
|
|
60
|
-
NotFound: {
|
|
61
|
-
description: 'Resource not found',
|
|
62
|
-
content: {
|
|
63
|
-
'application/vnd.api+json': {
|
|
64
|
-
schema: { $ref: '#/components/schemas/Error' }
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
ValidationError: {
|
|
69
|
-
description: 'Validation failed',
|
|
70
|
-
content: {
|
|
71
|
-
'application/vnd.api+json': {
|
|
72
|
-
schema: { $ref: '#/components/schemas/Error' }
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
security: [{ bearerAuth: [] }]
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// Add error schema
|
|
82
|
-
spec.components.schemas.Error = {
|
|
83
|
-
type: 'object',
|
|
84
|
-
properties: {
|
|
85
|
-
errors: {
|
|
86
|
-
type: 'array',
|
|
87
|
-
items: {
|
|
88
|
-
type: 'object',
|
|
89
|
-
properties: {
|
|
90
|
-
status: { type: 'string' },
|
|
91
|
-
title: { type: 'string' },
|
|
92
|
-
detail: { type: 'string' },
|
|
93
|
-
source: {
|
|
94
|
-
type: 'object',
|
|
95
|
-
properties: {
|
|
96
|
-
pointer: { type: 'string' }
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Generate schemas from registered schemas
|
|
106
|
-
for (const [type, schema] of api.schemas) {
|
|
107
|
-
const jsonSchema = schemaToJsonSchema(schema);
|
|
108
|
-
|
|
109
|
-
// Resource schema
|
|
110
|
-
spec.components.schemas[`${type}Resource`] = {
|
|
111
|
-
type: 'object',
|
|
112
|
-
properties: {
|
|
113
|
-
id: { type: 'string' },
|
|
114
|
-
type: { type: 'string', enum: [type] },
|
|
115
|
-
attributes: jsonSchema
|
|
116
|
-
},
|
|
117
|
-
required: ['type', 'attributes']
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Collection schema
|
|
121
|
-
spec.components.schemas[`${type}Collection`] = {
|
|
122
|
-
type: 'object',
|
|
123
|
-
properties: {
|
|
124
|
-
data: {
|
|
125
|
-
type: 'array',
|
|
126
|
-
items: { $ref: `#/components/schemas/${type}Resource` }
|
|
127
|
-
},
|
|
128
|
-
meta: {
|
|
129
|
-
type: 'object',
|
|
130
|
-
properties: {
|
|
131
|
-
total: { type: 'integer' },
|
|
132
|
-
pageSize: { type: 'integer' },
|
|
133
|
-
pageNumber: { type: 'integer' },
|
|
134
|
-
totalPages: { type: 'integer' }
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
links: {
|
|
138
|
-
type: 'object',
|
|
139
|
-
properties: {
|
|
140
|
-
self: { type: 'string' },
|
|
141
|
-
first: { type: 'string' },
|
|
142
|
-
last: { type: 'string' },
|
|
143
|
-
prev: { type: 'string' },
|
|
144
|
-
next: { type: 'string' }
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// Single resource response
|
|
151
|
-
spec.components.schemas[`${type}Response`] = {
|
|
152
|
-
type: 'object',
|
|
153
|
-
properties: {
|
|
154
|
-
data: { $ref: `#/components/schemas/${type}Resource` }
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Generate paths
|
|
159
|
-
spec.paths[`/${type}`] = {
|
|
160
|
-
get: {
|
|
161
|
-
tags: [type],
|
|
162
|
-
summary: `List ${type}`,
|
|
163
|
-
operationId: `list${capitalize(type)}`,
|
|
164
|
-
parameters: [
|
|
165
|
-
{
|
|
166
|
-
name: 'filter',
|
|
167
|
-
in: 'query',
|
|
168
|
-
style: 'deepObject',
|
|
169
|
-
explode: true,
|
|
170
|
-
schema: { type: 'object' },
|
|
171
|
-
description: 'Filter resources'
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
name: 'sort',
|
|
175
|
-
in: 'query',
|
|
176
|
-
schema: { type: 'string' },
|
|
177
|
-
description: 'Sort resources (use - for descending)'
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
name: 'page[size]',
|
|
181
|
-
in: 'query',
|
|
182
|
-
schema: { type: 'integer', default: 10 },
|
|
183
|
-
description: 'Page size'
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
name: 'page[number]',
|
|
187
|
-
in: 'query',
|
|
188
|
-
schema: { type: 'integer', default: 1 },
|
|
189
|
-
description: 'Page number'
|
|
190
|
-
}
|
|
191
|
-
],
|
|
192
|
-
responses: {
|
|
193
|
-
200: {
|
|
194
|
-
description: 'Success',
|
|
195
|
-
content: {
|
|
196
|
-
'application/vnd.api+json': {
|
|
197
|
-
schema: { $ref: `#/components/schemas/${type}Collection` }
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
post: {
|
|
204
|
-
tags: [type],
|
|
205
|
-
summary: `Create ${type}`,
|
|
206
|
-
operationId: `create${capitalize(type)}`,
|
|
207
|
-
requestBody: {
|
|
208
|
-
required: true,
|
|
209
|
-
content: {
|
|
210
|
-
'application/vnd.api+json': {
|
|
211
|
-
schema: {
|
|
212
|
-
type: 'object',
|
|
213
|
-
properties: {
|
|
214
|
-
data: {
|
|
215
|
-
type: 'object',
|
|
216
|
-
properties: {
|
|
217
|
-
type: { type: 'string', enum: [type] },
|
|
218
|
-
attributes: jsonSchema
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
responses: {
|
|
227
|
-
201: {
|
|
228
|
-
description: 'Created',
|
|
229
|
-
content: {
|
|
230
|
-
'application/vnd.api+json': {
|
|
231
|
-
schema: { $ref: `#/components/schemas/${type}Response` }
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
422: { $ref: '#/components/responses/ValidationError' }
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
spec.paths[`/${type}/{id}`] = {
|
|
241
|
-
parameters: [{ $ref: '#/components/parameters/idParam' }],
|
|
242
|
-
get: {
|
|
243
|
-
tags: [type],
|
|
244
|
-
summary: `Get ${type}`,
|
|
245
|
-
operationId: `get${capitalize(type)}`,
|
|
246
|
-
responses: {
|
|
247
|
-
200: {
|
|
248
|
-
description: 'Success',
|
|
249
|
-
content: {
|
|
250
|
-
'application/vnd.api+json': {
|
|
251
|
-
schema: { $ref: `#/components/schemas/${type}Response` }
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
404: { $ref: '#/components/responses/NotFound' }
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
patch: {
|
|
259
|
-
tags: [type],
|
|
260
|
-
summary: `Update ${type}`,
|
|
261
|
-
operationId: `update${capitalize(type)}`,
|
|
262
|
-
requestBody: {
|
|
263
|
-
required: true,
|
|
264
|
-
content: {
|
|
265
|
-
'application/vnd.api+json': {
|
|
266
|
-
schema: {
|
|
267
|
-
type: 'object',
|
|
268
|
-
properties: {
|
|
269
|
-
data: {
|
|
270
|
-
type: 'object',
|
|
271
|
-
properties: {
|
|
272
|
-
id: { type: 'string' },
|
|
273
|
-
type: { type: 'string', enum: [type] },
|
|
274
|
-
attributes: jsonSchema
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
responses: {
|
|
283
|
-
200: {
|
|
284
|
-
description: 'Success',
|
|
285
|
-
content: {
|
|
286
|
-
'application/vnd.api+json': {
|
|
287
|
-
schema: { $ref: `#/components/schemas/${type}Response` }
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
404: { $ref: '#/components/responses/NotFound' },
|
|
292
|
-
422: { $ref: '#/components/responses/ValidationError' }
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
delete: {
|
|
296
|
-
tags: [type],
|
|
297
|
-
summary: `Delete ${type}`,
|
|
298
|
-
operationId: `delete${capitalize(type)}`,
|
|
299
|
-
responses: {
|
|
300
|
-
204: { description: 'No Content' },
|
|
301
|
-
404: { $ref: '#/components/responses/NotFound' }
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return spec;
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// Serve OpenAPI spec
|
|
311
|
-
if (api.router) {
|
|
312
|
-
api.router.get('/openapi.json', (req, res) => {
|
|
313
|
-
res.json(api.generateOpenAPISpec());
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// Serve Swagger UI
|
|
317
|
-
api.router.get('/docs', (req, res) => {
|
|
318
|
-
res.send(`
|
|
319
|
-
<!DOCTYPE html>
|
|
320
|
-
<html>
|
|
321
|
-
<head>
|
|
322
|
-
<title>${defaultOptions.title}</title>
|
|
323
|
-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
|
|
324
|
-
</head>
|
|
325
|
-
<body>
|
|
326
|
-
<div id="swagger-ui"></div>
|
|
327
|
-
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
328
|
-
<script>
|
|
329
|
-
SwaggerUIBundle({
|
|
330
|
-
url: './openapi.json',
|
|
331
|
-
dom_id: '#swagger-ui',
|
|
332
|
-
deepLinking: true,
|
|
333
|
-
presets: [
|
|
334
|
-
SwaggerUIBundle.presets.apis,
|
|
335
|
-
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
336
|
-
]
|
|
337
|
-
});
|
|
338
|
-
</script>
|
|
339
|
-
</body>
|
|
340
|
-
</html>
|
|
341
|
-
`);
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// Convert Schema to JSON Schema
|
|
348
|
-
function schemaToJsonSchema(schema) {
|
|
349
|
-
const jsonSchema = {
|
|
350
|
-
type: 'object',
|
|
351
|
-
properties: {},
|
|
352
|
-
required: []
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
for (const [field, definition] of Object.entries(schema.structure)) {
|
|
356
|
-
const prop = { type: getJsonType(definition.type) };
|
|
357
|
-
|
|
358
|
-
// Add constraints
|
|
359
|
-
if (definition.min !== undefined) prop.minimum = definition.min;
|
|
360
|
-
if (definition.max !== undefined) prop.maximum = definition.max;
|
|
361
|
-
if (definition.enum) prop.enum = definition.enum;
|
|
362
|
-
if (definition.pattern) prop.pattern = definition.pattern;
|
|
363
|
-
if (definition.default !== undefined) prop.default = definition.default;
|
|
364
|
-
if (definition.description) prop.description = definition.description;
|
|
365
|
-
|
|
366
|
-
// String constraints
|
|
367
|
-
if (definition.type === 'string') {
|
|
368
|
-
if (definition.min) prop.minLength = definition.min;
|
|
369
|
-
if (definition.max) prop.maxLength = definition.max;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
jsonSchema.properties[field] = prop;
|
|
373
|
-
|
|
374
|
-
if (definition.required) {
|
|
375
|
-
jsonSchema.required.push(field);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return jsonSchema;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function getJsonType(schemaType) {
|
|
383
|
-
const typeMap = {
|
|
384
|
-
string: 'string',
|
|
385
|
-
number: 'number',
|
|
386
|
-
boolean: 'boolean',
|
|
387
|
-
id: 'string',
|
|
388
|
-
timestamp: 'integer',
|
|
389
|
-
date: 'string',
|
|
390
|
-
dateTime: 'string',
|
|
391
|
-
array: 'array',
|
|
392
|
-
object: 'object',
|
|
393
|
-
serialize: 'string',
|
|
394
|
-
blob: 'string'
|
|
395
|
-
};
|
|
396
|
-
return typeMap[schemaType] || 'string';
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function capitalize(str) {
|
|
400
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
401
|
-
}
|
package/plugins/positioning.js
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Positioning plugin for managing record order
|
|
3
|
-
*/
|
|
4
|
-
export const PositioningPlugin = {
|
|
5
|
-
install(api, options = {}) {
|
|
6
|
-
const defaultOptions = {
|
|
7
|
-
positionField: 'position',
|
|
8
|
-
beforeIdField: 'beforeId',
|
|
9
|
-
positionFilters: [],
|
|
10
|
-
...options
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
// Add positioning options to API
|
|
14
|
-
api.positioningOptions = defaultOptions;
|
|
15
|
-
|
|
16
|
-
// Hook into insert operations
|
|
17
|
-
api.hook('beforeInsert', async (context) => {
|
|
18
|
-
const { data, options } = context;
|
|
19
|
-
const posOptions = { ...defaultOptions, ...options.positioning };
|
|
20
|
-
|
|
21
|
-
if (!posOptions.enabled) return;
|
|
22
|
-
|
|
23
|
-
// Handle beforeId positioning
|
|
24
|
-
if (data[posOptions.beforeIdField] !== undefined) {
|
|
25
|
-
context.positioningData = {
|
|
26
|
-
beforeId: data[posOptions.beforeIdField],
|
|
27
|
-
positionField: posOptions.positionField,
|
|
28
|
-
filters: posOptions.positionFilters
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// Remove virtual field from data
|
|
32
|
-
delete data[posOptions.beforeIdField];
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Hook into update operations
|
|
37
|
-
api.hook('beforeUpdate', async (context) => {
|
|
38
|
-
const { data, options } = context;
|
|
39
|
-
const posOptions = { ...defaultOptions, ...options.positioning };
|
|
40
|
-
|
|
41
|
-
if (!posOptions.enabled) return;
|
|
42
|
-
|
|
43
|
-
// Handle beforeId positioning
|
|
44
|
-
if (data[posOptions.beforeIdField] !== undefined) {
|
|
45
|
-
context.positioningData = {
|
|
46
|
-
beforeId: data[posOptions.beforeIdField],
|
|
47
|
-
positionField: posOptions.positionField,
|
|
48
|
-
filters: posOptions.positionFilters
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Remove virtual field from data
|
|
52
|
-
delete data[posOptions.beforeIdField];
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Calculate and apply positioning after insert
|
|
57
|
-
api.hook('afterInsert', async (context) => {
|
|
58
|
-
if (!context.positioningData) return;
|
|
59
|
-
|
|
60
|
-
await applyPositioning(api, context, 'insert');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Calculate and apply positioning after update
|
|
64
|
-
api.hook('afterUpdate', async (context) => {
|
|
65
|
-
if (!context.positioningData) return;
|
|
66
|
-
|
|
67
|
-
await applyPositioning(api, context, 'update');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Add positioning helpers to API
|
|
71
|
-
api.reposition = async (type, recordId, beforeId, options = {}) => {
|
|
72
|
-
const posOptions = { ...defaultOptions, ...options };
|
|
73
|
-
|
|
74
|
-
// Update the record with new position
|
|
75
|
-
await api.update(recordId, {
|
|
76
|
-
[posOptions.beforeIdField]: beforeId
|
|
77
|
-
}, {
|
|
78
|
-
type,
|
|
79
|
-
positioning: { enabled: true, ...posOptions }
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Get next position in sequence
|
|
84
|
-
api.getNextPosition = async (type, filters = {}, options = {}) => {
|
|
85
|
-
const posOptions = { ...defaultOptions, ...options };
|
|
86
|
-
|
|
87
|
-
// Query to find max position
|
|
88
|
-
const result = await api.query({
|
|
89
|
-
filter: filters,
|
|
90
|
-
sort: `-${posOptions.positionField}`,
|
|
91
|
-
page: { size: 1 }
|
|
92
|
-
}, { type });
|
|
93
|
-
|
|
94
|
-
if (result.data.length > 0) {
|
|
95
|
-
const maxPosition = result.data[0].attributes[posOptions.positionField] || 0;
|
|
96
|
-
return maxPosition + 1;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return 1;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Normalize positions (remove gaps)
|
|
103
|
-
api.normalizePositions = async (type, filters = {}, options = {}) => {
|
|
104
|
-
const posOptions = { ...defaultOptions, ...options };
|
|
105
|
-
|
|
106
|
-
// Get all records sorted by position
|
|
107
|
-
const result = await api.query({
|
|
108
|
-
filter: filters,
|
|
109
|
-
sort: posOptions.positionField,
|
|
110
|
-
page: { size: 10000 } // Get all records
|
|
111
|
-
}, { type });
|
|
112
|
-
|
|
113
|
-
// Update positions to remove gaps
|
|
114
|
-
let position = 1;
|
|
115
|
-
for (const record of result.data) {
|
|
116
|
-
if (record.attributes[posOptions.positionField] !== position) {
|
|
117
|
-
await api.update(record.id, {
|
|
118
|
-
[posOptions.positionField]: position
|
|
119
|
-
}, { type });
|
|
120
|
-
}
|
|
121
|
-
position++;
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Apply positioning logic
|
|
129
|
-
*/
|
|
130
|
-
async function applyPositioning(api, context, operation) {
|
|
131
|
-
const { positioningData, result, options } = context;
|
|
132
|
-
const { beforeId, positionField, filters } = positioningData;
|
|
133
|
-
|
|
134
|
-
// Build filter for position queries
|
|
135
|
-
const positionFilter = { ...filters };
|
|
136
|
-
|
|
137
|
-
// Handle different beforeId values
|
|
138
|
-
if (beforeId === null) {
|
|
139
|
-
// Place at end - get max position
|
|
140
|
-
const maxPosResult = await api.query({
|
|
141
|
-
filter: positionFilter,
|
|
142
|
-
sort: `-${positionField}`,
|
|
143
|
-
page: { size: 1 }
|
|
144
|
-
}, options);
|
|
145
|
-
|
|
146
|
-
const maxPosition = maxPosResult.data.length > 0
|
|
147
|
-
? (maxPosResult.data[0].attributes[positionField] || 0)
|
|
148
|
-
: 0;
|
|
149
|
-
|
|
150
|
-
const newPosition = maxPosition + 1;
|
|
151
|
-
|
|
152
|
-
// Update the record with new position
|
|
153
|
-
await updatePosition(api, context, newPosition);
|
|
154
|
-
|
|
155
|
-
} else if (beforeId) {
|
|
156
|
-
// Place before specific record
|
|
157
|
-
const beforeRecord = await api.get(beforeId, options);
|
|
158
|
-
|
|
159
|
-
if (beforeRecord.data) {
|
|
160
|
-
const targetPosition = beforeRecord.data.attributes[positionField] || 1;
|
|
161
|
-
|
|
162
|
-
// Shift positions of records at or after target position
|
|
163
|
-
await shiftPositions(api, context, targetPosition, positionFilter);
|
|
164
|
-
|
|
165
|
-
// Update the record with target position
|
|
166
|
-
await updatePosition(api, context, targetPosition);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
// If beforeId is undefined, keep existing position (do nothing)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Shift positions to make room
|
|
174
|
-
*/
|
|
175
|
-
async function shiftPositions(api, context, fromPosition, filter) {
|
|
176
|
-
const { options } = context;
|
|
177
|
-
const { positionField } = context.positioningData;
|
|
178
|
-
|
|
179
|
-
// Get all records that need shifting
|
|
180
|
-
const result = await api.query({
|
|
181
|
-
filter: {
|
|
182
|
-
...filter,
|
|
183
|
-
[`${positionField}[gte]`]: fromPosition
|
|
184
|
-
},
|
|
185
|
-
sort: `-${positionField}`, // Sort descending to avoid conflicts
|
|
186
|
-
page: { size: 10000 }
|
|
187
|
-
}, options);
|
|
188
|
-
|
|
189
|
-
// Shift each record
|
|
190
|
-
for (const record of result.data) {
|
|
191
|
-
const currentPos = record.attributes[positionField];
|
|
192
|
-
await api.update(record.id, {
|
|
193
|
-
[positionField]: currentPos + 1
|
|
194
|
-
}, {
|
|
195
|
-
...options,
|
|
196
|
-
positioning: { enabled: false } // Disable positioning to avoid recursion
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Update record position
|
|
203
|
-
*/
|
|
204
|
-
async function updatePosition(api, context, position) {
|
|
205
|
-
const { result, options } = context;
|
|
206
|
-
const { positionField } = context.positioningData;
|
|
207
|
-
|
|
208
|
-
// Update the record with new position
|
|
209
|
-
const updateResult = await api.update(result.data.id, {
|
|
210
|
-
[positionField]: position
|
|
211
|
-
}, {
|
|
212
|
-
...options,
|
|
213
|
-
positioning: { enabled: false } // Disable positioning to avoid recursion
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Update the result in context
|
|
217
|
-
if (updateResult.data) {
|
|
218
|
-
context.result = updateResult;
|
|
219
|
-
}
|
|
220
|
-
}
|