s3db.js 12.1.0 → 12.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.
- package/README.md +212 -196
- package/dist/s3db.cjs.js +1286 -226
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1284 -226
- package/dist/s3db.es.js.map +1 -1
- package/package.json +6 -1
- package/src/cli/index.js +954 -43
- package/src/cli/migration-manager.js +270 -0
- package/src/concerns/calculator.js +0 -4
- package/src/concerns/metadata-encoding.js +1 -21
- package/src/concerns/plugin-storage.js +17 -4
- package/src/concerns/typescript-generator.d.ts +171 -0
- package/src/concerns/typescript-generator.js +275 -0
- package/src/database.class.js +171 -28
- package/src/index.js +15 -9
- package/src/plugins/api/index.js +0 -1
- package/src/plugins/api/routes/resource-routes.js +86 -1
- package/src/plugins/api/server.js +79 -3
- package/src/plugins/api/utils/openapi-generator.js +195 -5
- package/src/plugins/backup/multi-backup-driver.class.js +0 -1
- package/src/plugins/backup.plugin.js +7 -14
- package/src/plugins/concerns/plugin-dependencies.js +73 -19
- package/src/plugins/eventual-consistency/analytics.js +0 -2
- package/src/plugins/eventual-consistency/consolidation.js +2 -13
- package/src/plugins/eventual-consistency/index.js +0 -1
- package/src/plugins/eventual-consistency/install.js +1 -1
- package/src/plugins/geo.plugin.js +5 -6
- package/src/plugins/importer/index.js +1 -1
- package/src/plugins/relation.plugin.js +11 -11
- package/src/plugins/replicator.plugin.js +12 -21
- package/src/plugins/s3-queue.plugin.js +4 -4
- package/src/plugins/scheduler.plugin.js +10 -12
- package/src/plugins/state-machine.plugin.js +8 -12
- package/src/plugins/tfstate/README.md +1 -1
- package/src/plugins/tfstate/errors.js +3 -3
- package/src/plugins/tfstate/index.js +41 -67
- package/src/plugins/ttl.plugin.js +3 -3
- package/src/resource.class.js +263 -61
- package/src/schema.class.js +0 -2
- package/src/testing/factory.class.js +286 -0
- package/src/testing/index.js +15 -0
- package/src/testing/seeder.class.js +183 -0
|
@@ -299,6 +299,91 @@ export function createResourceRoutes(resource, version, config = {}) {
|
|
|
299
299
|
return app;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Create relational routes for a resource relation
|
|
304
|
+
* @param {Object} sourceResource - Source s3db.js Resource instance
|
|
305
|
+
* @param {string} relationName - Name of the relation (e.g., 'posts', 'profile')
|
|
306
|
+
* @param {Object} relationConfig - Relation configuration from RelationPlugin
|
|
307
|
+
* @param {string} version - Resource version (e.g., 'v1')
|
|
308
|
+
* @returns {Hono} Hono app with relational routes
|
|
309
|
+
*/
|
|
310
|
+
export function createRelationalRoutes(sourceResource, relationName, relationConfig, version) {
|
|
311
|
+
const app = new Hono();
|
|
312
|
+
const resourceName = sourceResource.name;
|
|
313
|
+
const relatedResourceName = relationConfig.resource;
|
|
314
|
+
|
|
315
|
+
// GET /{version}/{resource}/:id/{relation}
|
|
316
|
+
// Examples: GET /v1/users/user123/posts, GET /v1/users/user123/profile
|
|
317
|
+
app.get('/:id', asyncHandler(async (c) => {
|
|
318
|
+
const id = c.req.param('id');
|
|
319
|
+
const query = c.req.query();
|
|
320
|
+
|
|
321
|
+
// Check if source resource exists
|
|
322
|
+
const source = await sourceResource.get(id);
|
|
323
|
+
if (!source) {
|
|
324
|
+
const response = formatter.notFound(resourceName, id);
|
|
325
|
+
return c.json(response, response._status);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Use RelationPlugin's include feature to load the relation
|
|
329
|
+
const result = await sourceResource.get(id, {
|
|
330
|
+
include: [relationName]
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const relatedData = result[relationName];
|
|
334
|
+
|
|
335
|
+
// Check if relation exists
|
|
336
|
+
if (!relatedData) {
|
|
337
|
+
// Return appropriate response based on relation type
|
|
338
|
+
if (relationConfig.type === 'hasMany' || relationConfig.type === 'belongsToMany') {
|
|
339
|
+
// For *-to-many relations, return empty array
|
|
340
|
+
const response = formatter.list([], {
|
|
341
|
+
total: 0,
|
|
342
|
+
page: 1,
|
|
343
|
+
pageSize: 100,
|
|
344
|
+
pageCount: 0
|
|
345
|
+
});
|
|
346
|
+
return c.json(response, response._status);
|
|
347
|
+
} else {
|
|
348
|
+
// For *-to-one relations, return 404
|
|
349
|
+
const response = formatter.notFound(relatedResourceName, 'related resource');
|
|
350
|
+
return c.json(response, response._status);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Return appropriate format based on relation type
|
|
355
|
+
if (relationConfig.type === 'hasMany' || relationConfig.type === 'belongsToMany') {
|
|
356
|
+
// For *-to-many, return list format
|
|
357
|
+
const items = Array.isArray(relatedData) ? relatedData : [relatedData];
|
|
358
|
+
const limit = parseInt(query.limit) || 100;
|
|
359
|
+
const offset = parseInt(query.offset) || 0;
|
|
360
|
+
|
|
361
|
+
// Apply pagination
|
|
362
|
+
const paginatedItems = items.slice(offset, offset + limit);
|
|
363
|
+
|
|
364
|
+
const response = formatter.list(paginatedItems, {
|
|
365
|
+
total: items.length,
|
|
366
|
+
page: Math.floor(offset / limit) + 1,
|
|
367
|
+
pageSize: limit,
|
|
368
|
+
pageCount: Math.ceil(items.length / limit)
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Set pagination headers
|
|
372
|
+
c.header('X-Total-Count', items.length.toString());
|
|
373
|
+
c.header('X-Page-Count', Math.ceil(items.length / limit).toString());
|
|
374
|
+
|
|
375
|
+
return c.json(response, response._status);
|
|
376
|
+
} else {
|
|
377
|
+
// For *-to-one, return single resource format
|
|
378
|
+
const response = formatter.success(relatedData);
|
|
379
|
+
return c.json(response, response._status);
|
|
380
|
+
}
|
|
381
|
+
}));
|
|
382
|
+
|
|
383
|
+
return app;
|
|
384
|
+
}
|
|
385
|
+
|
|
302
386
|
export default {
|
|
303
|
-
createResourceRoutes
|
|
387
|
+
createResourceRoutes,
|
|
388
|
+
createRelationalRoutes
|
|
304
389
|
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { Hono } from 'hono';
|
|
8
8
|
import { serve } from '@hono/node-server';
|
|
9
9
|
import { swaggerUI } from '@hono/swagger-ui';
|
|
10
|
-
import { createResourceRoutes } from './routes/resource-routes.js';
|
|
10
|
+
import { createResourceRoutes, createRelationalRoutes } from './routes/resource-routes.js';
|
|
11
11
|
import { errorHandler } from './utils/error-handler.js';
|
|
12
12
|
import * as formatter from './utils/response-formatter.js';
|
|
13
13
|
import { generateOpenAPISpec } from './utils/openapi-generator.js';
|
|
@@ -51,6 +51,11 @@ export class ApiServer {
|
|
|
51
51
|
this.isRunning = false;
|
|
52
52
|
this.openAPISpec = null;
|
|
53
53
|
|
|
54
|
+
// Detect if RelationPlugin is installed
|
|
55
|
+
this.relationsPlugin = this.options.database?.plugins?.relation ||
|
|
56
|
+
this.options.database?.plugins?.RelationPlugin ||
|
|
57
|
+
null;
|
|
58
|
+
|
|
54
59
|
this._setupRoutes();
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -165,12 +170,10 @@ export class ApiServer {
|
|
|
165
170
|
|
|
166
171
|
// API Documentation UI endpoint
|
|
167
172
|
if (this.options.docsUI === 'swagger') {
|
|
168
|
-
// Swagger UI (legacy, less pretty)
|
|
169
173
|
this.app.get('/docs', swaggerUI({
|
|
170
174
|
url: '/openapi.json'
|
|
171
175
|
}));
|
|
172
176
|
} else {
|
|
173
|
-
// Redoc (modern, beautiful design!)
|
|
174
177
|
this.app.get('/docs', (c) => {
|
|
175
178
|
return c.html(`<!DOCTYPE html>
|
|
176
179
|
<html lang="en">
|
|
@@ -197,6 +200,11 @@ export class ApiServer {
|
|
|
197
200
|
// Setup resource routes
|
|
198
201
|
this._setupResourceRoutes();
|
|
199
202
|
|
|
203
|
+
// Setup relational routes if RelationPlugin is active
|
|
204
|
+
if (this.relationsPlugin) {
|
|
205
|
+
this._setupRelationalRoutes();
|
|
206
|
+
}
|
|
207
|
+
|
|
200
208
|
// Global error handler
|
|
201
209
|
this.app.onError((err, c) => {
|
|
202
210
|
return errorHandler(err, c);
|
|
@@ -257,6 +265,74 @@ export class ApiServer {
|
|
|
257
265
|
}
|
|
258
266
|
}
|
|
259
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Setup relational routes (when RelationPlugin is active)
|
|
270
|
+
* @private
|
|
271
|
+
*/
|
|
272
|
+
_setupRelationalRoutes() {
|
|
273
|
+
if (!this.relationsPlugin || !this.relationsPlugin.relations) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { database } = this.options;
|
|
278
|
+
const relations = this.relationsPlugin.relations;
|
|
279
|
+
|
|
280
|
+
if (this.options.verbose) {
|
|
281
|
+
console.log('[API Plugin] Setting up relational routes...');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const [resourceName, relationsDef] of Object.entries(relations)) {
|
|
285
|
+
const resource = database.resources[resourceName];
|
|
286
|
+
if (!resource) {
|
|
287
|
+
if (this.options.verbose) {
|
|
288
|
+
console.warn(`[API Plugin] Resource '${resourceName}' not found for relational routes`);
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Skip plugin resources unless explicitly included
|
|
294
|
+
if (resourceName.startsWith('plg_') && !this.options.resources[resourceName]) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const version = resource.config?.currentVersion || resource.version || 'v1';
|
|
299
|
+
|
|
300
|
+
for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
|
|
301
|
+
// Only create routes for relations that should be exposed via API
|
|
302
|
+
// Skip belongsTo relations (they're just reverse lookups, not useful as endpoints)
|
|
303
|
+
if (relationConfig.type === 'belongsTo') {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check if relation should be exposed (default: yes, unless explicitly disabled)
|
|
308
|
+
const resourceConfig = this.options.resources[resourceName];
|
|
309
|
+
const exposeRelation = resourceConfig?.relations?.[relationName]?.expose !== false;
|
|
310
|
+
|
|
311
|
+
if (!exposeRelation) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Create relational routes
|
|
316
|
+
const relationalApp = createRelationalRoutes(
|
|
317
|
+
resource,
|
|
318
|
+
relationName,
|
|
319
|
+
relationConfig,
|
|
320
|
+
version
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Mount relational routes at /{version}/{resource}/:id/{relation}
|
|
324
|
+
this.app.route(`/${version}/${resourceName}/:id/${relationName}`, relationalApp);
|
|
325
|
+
|
|
326
|
+
if (this.options.verbose) {
|
|
327
|
+
console.log(
|
|
328
|
+
`[API Plugin] Mounted relational route: /${version}/${resourceName}/:id/${relationName} ` +
|
|
329
|
+
`(${relationConfig.type} -> ${relationConfig.resource})`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
260
336
|
/**
|
|
261
337
|
* Start the server
|
|
262
338
|
* @returns {Promise<void>}
|
|
@@ -100,6 +100,12 @@ function generateResourceSchema(resource) {
|
|
|
100
100
|
|
|
101
101
|
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
102
102
|
|
|
103
|
+
// Extract resource description (supports both string and object format)
|
|
104
|
+
const resourceDescription = resource.config?.description;
|
|
105
|
+
const attributeDescriptions = typeof resourceDescription === 'object'
|
|
106
|
+
? (resourceDescription.attributes || {})
|
|
107
|
+
: {};
|
|
108
|
+
|
|
103
109
|
// Add system-generated id field (always present in responses)
|
|
104
110
|
properties.id = {
|
|
105
111
|
type: 'string',
|
|
@@ -114,7 +120,7 @@ function generateResourceSchema(resource) {
|
|
|
114
120
|
const baseType = mapFieldTypeToOpenAPI(fieldDef.type);
|
|
115
121
|
properties[fieldName] = {
|
|
116
122
|
...baseType,
|
|
117
|
-
description: fieldDef.description || undefined
|
|
123
|
+
description: fieldDef.description || attributeDescriptions[fieldName] || undefined
|
|
118
124
|
};
|
|
119
125
|
|
|
120
126
|
if (fieldDef.required) {
|
|
@@ -142,7 +148,8 @@ function generateResourceSchema(resource) {
|
|
|
142
148
|
|
|
143
149
|
properties[fieldName] = {
|
|
144
150
|
...baseType,
|
|
145
|
-
...rules
|
|
151
|
+
...rules,
|
|
152
|
+
description: attributeDescriptions[fieldName] || undefined
|
|
146
153
|
};
|
|
147
154
|
|
|
148
155
|
if (rules.required) {
|
|
@@ -787,6 +794,113 @@ The response includes pagination metadata in the \`pagination\` object with tota
|
|
|
787
794
|
return paths;
|
|
788
795
|
}
|
|
789
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Generate OpenAPI paths for relational routes
|
|
799
|
+
* @param {Object} resource - Source s3db.js Resource instance
|
|
800
|
+
* @param {string} relationName - Name of the relation
|
|
801
|
+
* @param {Object} relationConfig - Relation configuration
|
|
802
|
+
* @param {string} version - Resource version
|
|
803
|
+
* @param {Object} relatedSchema - OpenAPI schema for related resource
|
|
804
|
+
* @returns {Object} OpenAPI paths for relation
|
|
805
|
+
*/
|
|
806
|
+
function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema) {
|
|
807
|
+
const resourceName = resource.name;
|
|
808
|
+
const basePath = `/${version}/${resourceName}/{id}/${relationName}`;
|
|
809
|
+
const relatedResourceName = relationConfig.resource;
|
|
810
|
+
const isToMany = relationConfig.type === 'hasMany' || relationConfig.type === 'belongsToMany';
|
|
811
|
+
|
|
812
|
+
const paths = {};
|
|
813
|
+
|
|
814
|
+
paths[basePath] = {
|
|
815
|
+
get: {
|
|
816
|
+
tags: [resourceName],
|
|
817
|
+
summary: `Get ${relationName} of ${resourceName}`,
|
|
818
|
+
description: `Retrieve ${relationName} (${relationConfig.type}) associated with this ${resourceName}. ` +
|
|
819
|
+
`This endpoint uses the RelationPlugin to efficiently load related data` +
|
|
820
|
+
(relationConfig.partitionHint ? ` via the '${relationConfig.partitionHint}' partition.` : '.'),
|
|
821
|
+
parameters: [
|
|
822
|
+
{
|
|
823
|
+
name: 'id',
|
|
824
|
+
in: 'path',
|
|
825
|
+
required: true,
|
|
826
|
+
description: `${resourceName} ID`,
|
|
827
|
+
schema: { type: 'string' }
|
|
828
|
+
},
|
|
829
|
+
...(isToMany ? [
|
|
830
|
+
{
|
|
831
|
+
name: 'limit',
|
|
832
|
+
in: 'query',
|
|
833
|
+
description: 'Maximum number of items to return',
|
|
834
|
+
schema: { type: 'integer', default: 100, minimum: 1, maximum: 1000 }
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: 'offset',
|
|
838
|
+
in: 'query',
|
|
839
|
+
description: 'Number of items to skip',
|
|
840
|
+
schema: { type: 'integer', default: 0, minimum: 0 }
|
|
841
|
+
}
|
|
842
|
+
] : [])
|
|
843
|
+
],
|
|
844
|
+
responses: {
|
|
845
|
+
200: {
|
|
846
|
+
description: 'Successful response',
|
|
847
|
+
content: {
|
|
848
|
+
'application/json': {
|
|
849
|
+
schema: isToMany ? {
|
|
850
|
+
type: 'object',
|
|
851
|
+
properties: {
|
|
852
|
+
success: { type: 'boolean', example: true },
|
|
853
|
+
data: {
|
|
854
|
+
type: 'array',
|
|
855
|
+
items: relatedSchema
|
|
856
|
+
},
|
|
857
|
+
pagination: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
properties: {
|
|
860
|
+
total: { type: 'integer' },
|
|
861
|
+
page: { type: 'integer' },
|
|
862
|
+
pageSize: { type: 'integer' },
|
|
863
|
+
pageCount: { type: 'integer' }
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
} : {
|
|
868
|
+
type: 'object',
|
|
869
|
+
properties: {
|
|
870
|
+
success: { type: 'boolean', example: true },
|
|
871
|
+
data: relatedSchema
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
...(isToMany ? {
|
|
877
|
+
headers: {
|
|
878
|
+
'X-Total-Count': {
|
|
879
|
+
description: 'Total number of related records',
|
|
880
|
+
schema: { type: 'integer' }
|
|
881
|
+
},
|
|
882
|
+
'X-Page-Count': {
|
|
883
|
+
description: 'Total number of pages',
|
|
884
|
+
schema: { type: 'integer' }
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
} : {})
|
|
888
|
+
},
|
|
889
|
+
404: {
|
|
890
|
+
description: `${resourceName} not found` + (isToMany ? '' : ' or no related resource exists'),
|
|
891
|
+
content: {
|
|
892
|
+
'application/json': {
|
|
893
|
+
schema: { $ref: '#/components/schemas/Error' }
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
return paths;
|
|
902
|
+
}
|
|
903
|
+
|
|
790
904
|
/**
|
|
791
905
|
* Generate complete OpenAPI 3.0 specification
|
|
792
906
|
* @param {Object} database - s3db.js Database instance
|
|
@@ -803,12 +917,42 @@ export function generateOpenAPISpec(database, config = {}) {
|
|
|
803
917
|
resources: resourceConfigs = {}
|
|
804
918
|
} = config;
|
|
805
919
|
|
|
920
|
+
// Build resources table for description
|
|
921
|
+
const resourcesTableRows = [];
|
|
922
|
+
for (const [name, resource] of Object.entries(database.resources)) {
|
|
923
|
+
// Skip plugin resources unless explicitly configured
|
|
924
|
+
if (name.startsWith('plg_') && !resourceConfigs[name]) {
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const version = resource.config?.currentVersion || resource.version || 'v1';
|
|
929
|
+
const resourceDescription = resource.config?.description;
|
|
930
|
+
const descText = typeof resourceDescription === 'object'
|
|
931
|
+
? resourceDescription.resource
|
|
932
|
+
: resourceDescription || 'No description';
|
|
933
|
+
|
|
934
|
+
resourcesTableRows.push(`| ${name} | ${descText} | \`/${version}/${name}\` |`);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Build enhanced description with resources table
|
|
938
|
+
const enhancedDescription = `${description}
|
|
939
|
+
|
|
940
|
+
## Available Resources
|
|
941
|
+
|
|
942
|
+
| Resource | Description | Base Path |
|
|
943
|
+
|----------|-------------|-----------|
|
|
944
|
+
${resourcesTableRows.join('\n')}
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
For detailed information about each endpoint, see the sections below.`;
|
|
949
|
+
|
|
806
950
|
const spec = {
|
|
807
951
|
openapi: '3.1.0',
|
|
808
952
|
info: {
|
|
809
953
|
title,
|
|
810
954
|
version,
|
|
811
|
-
description,
|
|
955
|
+
description: enhancedDescription,
|
|
812
956
|
contact: {
|
|
813
957
|
name: 's3db.js',
|
|
814
958
|
url: 'https://github.com/forattini-dev/s3db.js'
|
|
@@ -903,6 +1047,9 @@ export function generateOpenAPISpec(database, config = {}) {
|
|
|
903
1047
|
// Generate paths for each resource
|
|
904
1048
|
const resources = database.resources;
|
|
905
1049
|
|
|
1050
|
+
// Detect RelationPlugin
|
|
1051
|
+
const relationsPlugin = database.plugins?.relation || database.plugins?.RelationPlugin || null;
|
|
1052
|
+
|
|
906
1053
|
for (const [name, resource] of Object.entries(resources)) {
|
|
907
1054
|
// Skip plugin resources unless explicitly configured
|
|
908
1055
|
if (name.startsWith('plg_') && !resourceConfigs[name]) {
|
|
@@ -924,14 +1071,57 @@ export function generateOpenAPISpec(database, config = {}) {
|
|
|
924
1071
|
// Merge paths
|
|
925
1072
|
Object.assign(spec.paths, paths);
|
|
926
1073
|
|
|
927
|
-
// Add tag
|
|
1074
|
+
// Add tag with description support
|
|
1075
|
+
const resourceDescription = resource.config?.description;
|
|
1076
|
+
const tagDescription = typeof resourceDescription === 'object'
|
|
1077
|
+
? resourceDescription.resource
|
|
1078
|
+
: resourceDescription || `Operations for ${name} resource`;
|
|
1079
|
+
|
|
928
1080
|
spec.tags.push({
|
|
929
1081
|
name: name,
|
|
930
|
-
description:
|
|
1082
|
+
description: tagDescription
|
|
931
1083
|
});
|
|
932
1084
|
|
|
933
1085
|
// Add schema to components
|
|
934
1086
|
spec.components.schemas[name] = generateResourceSchema(resource);
|
|
1087
|
+
|
|
1088
|
+
// Generate relational paths if RelationPlugin is active
|
|
1089
|
+
if (relationsPlugin && relationsPlugin.relations && relationsPlugin.relations[name]) {
|
|
1090
|
+
const relationsDef = relationsPlugin.relations[name];
|
|
1091
|
+
|
|
1092
|
+
for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
|
|
1093
|
+
// Skip belongsTo relations (not useful as REST endpoints)
|
|
1094
|
+
if (relationConfig.type === 'belongsTo') {
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Check if relation should be exposed (default: yes)
|
|
1099
|
+
const exposeRelation = config?.relations?.[relationName]?.expose !== false;
|
|
1100
|
+
if (!exposeRelation) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Get related resource schema
|
|
1105
|
+
const relatedResource = database.resources[relationConfig.resource];
|
|
1106
|
+
if (!relatedResource) {
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const relatedSchema = generateResourceSchema(relatedResource);
|
|
1111
|
+
|
|
1112
|
+
// Generate relational paths
|
|
1113
|
+
const relationalPaths = generateRelationalPaths(
|
|
1114
|
+
resource,
|
|
1115
|
+
relationName,
|
|
1116
|
+
relationConfig,
|
|
1117
|
+
version,
|
|
1118
|
+
relatedSchema
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
// Merge relational paths
|
|
1122
|
+
Object.assign(spec.paths, relationalPaths);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
935
1125
|
}
|
|
936
1126
|
|
|
937
1127
|
// Add authentication endpoints if enabled
|
|
@@ -333,7 +333,7 @@ export class BackupPlugin extends Plugin {
|
|
|
333
333
|
};
|
|
334
334
|
|
|
335
335
|
const [ok] = await tryFn(() =>
|
|
336
|
-
this.database.
|
|
336
|
+
this.database.resources[this.config.backupMetadataResource].insert(metadata)
|
|
337
337
|
);
|
|
338
338
|
|
|
339
339
|
return metadata;
|
|
@@ -341,7 +341,7 @@ export class BackupPlugin extends Plugin {
|
|
|
341
341
|
|
|
342
342
|
async _updateBackupMetadata(backupId, updates) {
|
|
343
343
|
const [ok] = await tryFn(() =>
|
|
344
|
-
this.database.
|
|
344
|
+
this.database.resources[this.config.backupMetadataResource].update(backupId, updates)
|
|
345
345
|
);
|
|
346
346
|
}
|
|
347
347
|
|
|
@@ -387,7 +387,7 @@ export class BackupPlugin extends Plugin {
|
|
|
387
387
|
let sinceTimestamp = null;
|
|
388
388
|
if (type === 'incremental') {
|
|
389
389
|
const [lastBackupOk, , lastBackups] = await tryFn(() =>
|
|
390
|
-
this.database.
|
|
390
|
+
this.database.resources[this.config.backupMetadataResource].list({
|
|
391
391
|
filter: {
|
|
392
392
|
status: 'completed',
|
|
393
393
|
type: { $in: ['full', 'incremental'] }
|
|
@@ -802,7 +802,7 @@ export class BackupPlugin extends Plugin {
|
|
|
802
802
|
|
|
803
803
|
// Merge with metadata from database
|
|
804
804
|
const [metaOk, , metadataRecords] = await tryFn(() =>
|
|
805
|
-
this.database.
|
|
805
|
+
this.database.resources[this.config.backupMetadataResource].list({
|
|
806
806
|
limit: options.limit || 50,
|
|
807
807
|
sort: { timestamp: -1 }
|
|
808
808
|
})
|
|
@@ -836,7 +836,7 @@ export class BackupPlugin extends Plugin {
|
|
|
836
836
|
*/
|
|
837
837
|
async getBackupStatus(backupId) {
|
|
838
838
|
const [ok, , backup] = await tryFn(() =>
|
|
839
|
-
this.database.
|
|
839
|
+
this.database.resources[this.config.backupMetadataResource].get(backupId)
|
|
840
840
|
);
|
|
841
841
|
|
|
842
842
|
return ok ? backup : null;
|
|
@@ -846,7 +846,7 @@ export class BackupPlugin extends Plugin {
|
|
|
846
846
|
try {
|
|
847
847
|
// Get all completed backups sorted by timestamp
|
|
848
848
|
const [listOk, , allBackups] = await tryFn(() =>
|
|
849
|
-
this.database.
|
|
849
|
+
this.database.resources[this.config.backupMetadataResource].list({
|
|
850
850
|
filter: { status: 'completed' },
|
|
851
851
|
sort: { timestamp: -1 }
|
|
852
852
|
})
|
|
@@ -938,7 +938,7 @@ export class BackupPlugin extends Plugin {
|
|
|
938
938
|
await this.driver.delete(backup.id, backup.driverInfo);
|
|
939
939
|
|
|
940
940
|
// Delete metadata
|
|
941
|
-
await this.database.
|
|
941
|
+
await this.database.resources[this.config.backupMetadataResource].delete(backup.id);
|
|
942
942
|
|
|
943
943
|
if (this.config.verbose) {
|
|
944
944
|
console.log(`[BackupPlugin] Deleted old backup: ${backup.id}`);
|
|
@@ -982,13 +982,6 @@ export class BackupPlugin extends Plugin {
|
|
|
982
982
|
await this.driver.cleanup();
|
|
983
983
|
}
|
|
984
984
|
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* Cleanup plugin resources (alias for stop for backward compatibility)
|
|
988
|
-
*/
|
|
989
|
-
async cleanup() {
|
|
990
|
-
await this.stop();
|
|
991
|
-
}
|
|
992
985
|
}
|
|
993
986
|
|
|
994
987
|
export default BackupPlugin;
|