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
package/dist/s3db.es.js
CHANGED
|
@@ -7,7 +7,7 @@ import { swaggerUI } from '@hono/swagger-ui';
|
|
|
7
7
|
import { mkdir, copyFile, unlink, stat, access, readdir, writeFile, readFile, rm, watch } from 'fs/promises';
|
|
8
8
|
import fs, { createReadStream, createWriteStream, realpathSync as realpathSync$1, readlinkSync, readdirSync, readdir as readdir$2, lstatSync, existsSync } from 'fs';
|
|
9
9
|
import { pipeline } from 'stream/promises';
|
|
10
|
-
import path$1, { join } from 'path';
|
|
10
|
+
import path$1, { join, dirname } from 'path';
|
|
11
11
|
import { Transform, Writable } from 'stream';
|
|
12
12
|
import zlib from 'node:zlib';
|
|
13
13
|
import os from 'os';
|
|
@@ -209,8 +209,6 @@ function calculateUTF8Bytes(str) {
|
|
|
209
209
|
function clearUTF8Memory() {
|
|
210
210
|
utf8BytesMemory.clear();
|
|
211
211
|
}
|
|
212
|
-
const clearUTF8Memo = clearUTF8Memory;
|
|
213
|
-
const clearUTF8Cache = clearUTF8Memory;
|
|
214
212
|
function calculateAttributeNamesSize(mappedObject) {
|
|
215
213
|
let totalSize = 0;
|
|
216
214
|
for (const key of Object.keys(mappedObject)) {
|
|
@@ -1604,18 +1602,6 @@ function metadataDecode(value) {
|
|
|
1604
1602
|
}
|
|
1605
1603
|
}
|
|
1606
1604
|
}
|
|
1607
|
-
const len = value.length;
|
|
1608
|
-
if (len > 0 && len % 4 === 0) {
|
|
1609
|
-
if (/^[A-Za-z0-9+/]+=*$/.test(value)) {
|
|
1610
|
-
try {
|
|
1611
|
-
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
1612
|
-
if (/[^\x00-\x7F]/.test(decoded) && Buffer.from(decoded, "utf8").toString("base64") === value) {
|
|
1613
|
-
return decoded;
|
|
1614
|
-
}
|
|
1615
|
-
} catch {
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
1605
|
return value;
|
|
1620
1606
|
}
|
|
1621
1607
|
|
|
@@ -1702,11 +1688,22 @@ class PluginStorage {
|
|
|
1702
1688
|
}
|
|
1703
1689
|
}
|
|
1704
1690
|
/**
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
1691
|
+
* Batch set multiple items
|
|
1692
|
+
*
|
|
1693
|
+
* @param {Array<{key: string, data: Object, options?: Object}>} items - Items to save
|
|
1694
|
+
* @returns {Promise<Array<{ok: boolean, key: string, error?: Error}>>} Results
|
|
1707
1695
|
*/
|
|
1708
|
-
async
|
|
1709
|
-
|
|
1696
|
+
async batchSet(items) {
|
|
1697
|
+
const results = [];
|
|
1698
|
+
for (const item of items) {
|
|
1699
|
+
try {
|
|
1700
|
+
await this.set(item.key, item.data, item.options || {});
|
|
1701
|
+
results.push({ ok: true, key: item.key });
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
results.push({ ok: false, key: item.key, error });
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return results;
|
|
1710
1707
|
}
|
|
1711
1708
|
/**
|
|
1712
1709
|
* Get data with automatic metadata decoding and TTL check
|
|
@@ -2821,6 +2818,57 @@ function createResourceRoutes(resource, version, config = {}) {
|
|
|
2821
2818
|
}
|
|
2822
2819
|
return app;
|
|
2823
2820
|
}
|
|
2821
|
+
function createRelationalRoutes(sourceResource, relationName, relationConfig, version) {
|
|
2822
|
+
const app = new Hono();
|
|
2823
|
+
const resourceName = sourceResource.name;
|
|
2824
|
+
const relatedResourceName = relationConfig.resource;
|
|
2825
|
+
app.get("/:id", asyncHandler(async (c) => {
|
|
2826
|
+
const id = c.req.param("id");
|
|
2827
|
+
const query = c.req.query();
|
|
2828
|
+
const source = await sourceResource.get(id);
|
|
2829
|
+
if (!source) {
|
|
2830
|
+
const response = notFound(resourceName, id);
|
|
2831
|
+
return c.json(response, response._status);
|
|
2832
|
+
}
|
|
2833
|
+
const result = await sourceResource.get(id, {
|
|
2834
|
+
include: [relationName]
|
|
2835
|
+
});
|
|
2836
|
+
const relatedData = result[relationName];
|
|
2837
|
+
if (!relatedData) {
|
|
2838
|
+
if (relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany") {
|
|
2839
|
+
const response = list([], {
|
|
2840
|
+
total: 0,
|
|
2841
|
+
page: 1,
|
|
2842
|
+
pageSize: 100,
|
|
2843
|
+
pageCount: 0
|
|
2844
|
+
});
|
|
2845
|
+
return c.json(response, response._status);
|
|
2846
|
+
} else {
|
|
2847
|
+
const response = notFound(relatedResourceName, "related resource");
|
|
2848
|
+
return c.json(response, response._status);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany") {
|
|
2852
|
+
const items = Array.isArray(relatedData) ? relatedData : [relatedData];
|
|
2853
|
+
const limit = parseInt(query.limit) || 100;
|
|
2854
|
+
const offset = parseInt(query.offset) || 0;
|
|
2855
|
+
const paginatedItems = items.slice(offset, offset + limit);
|
|
2856
|
+
const response = list(paginatedItems, {
|
|
2857
|
+
total: items.length,
|
|
2858
|
+
page: Math.floor(offset / limit) + 1,
|
|
2859
|
+
pageSize: limit,
|
|
2860
|
+
pageCount: Math.ceil(items.length / limit)
|
|
2861
|
+
});
|
|
2862
|
+
c.header("X-Total-Count", items.length.toString());
|
|
2863
|
+
c.header("X-Page-Count", Math.ceil(items.length / limit).toString());
|
|
2864
|
+
return c.json(response, response._status);
|
|
2865
|
+
} else {
|
|
2866
|
+
const response = success(relatedData);
|
|
2867
|
+
return c.json(response, response._status);
|
|
2868
|
+
}
|
|
2869
|
+
}));
|
|
2870
|
+
return app;
|
|
2871
|
+
}
|
|
2824
2872
|
|
|
2825
2873
|
function mapFieldTypeToOpenAPI(fieldType) {
|
|
2826
2874
|
const type = fieldType.split("|")[0].trim();
|
|
@@ -2891,6 +2939,8 @@ function generateResourceSchema(resource) {
|
|
|
2891
2939
|
const properties = {};
|
|
2892
2940
|
const required = [];
|
|
2893
2941
|
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
2942
|
+
const resourceDescription = resource.config?.description;
|
|
2943
|
+
const attributeDescriptions = typeof resourceDescription === "object" ? resourceDescription.attributes || {} : {};
|
|
2894
2944
|
properties.id = {
|
|
2895
2945
|
type: "string",
|
|
2896
2946
|
description: "Unique identifier for the resource",
|
|
@@ -2902,7 +2952,7 @@ function generateResourceSchema(resource) {
|
|
|
2902
2952
|
const baseType = mapFieldTypeToOpenAPI(fieldDef.type);
|
|
2903
2953
|
properties[fieldName] = {
|
|
2904
2954
|
...baseType,
|
|
2905
|
-
description: fieldDef.description || void 0
|
|
2955
|
+
description: fieldDef.description || attributeDescriptions[fieldName] || void 0
|
|
2906
2956
|
};
|
|
2907
2957
|
if (fieldDef.required) {
|
|
2908
2958
|
required.push(fieldName);
|
|
@@ -2922,7 +2972,8 @@ function generateResourceSchema(resource) {
|
|
|
2922
2972
|
const rules = extractValidationRules(fieldDef);
|
|
2923
2973
|
properties[fieldName] = {
|
|
2924
2974
|
...baseType,
|
|
2925
|
-
...rules
|
|
2975
|
+
...rules,
|
|
2976
|
+
description: attributeDescriptions[fieldName] || void 0
|
|
2926
2977
|
};
|
|
2927
2978
|
if (rules.required) {
|
|
2928
2979
|
required.push(fieldName);
|
|
@@ -3504,6 +3555,98 @@ The response includes pagination metadata in the \`pagination\` object with tota
|
|
|
3504
3555
|
}
|
|
3505
3556
|
return paths;
|
|
3506
3557
|
}
|
|
3558
|
+
function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema) {
|
|
3559
|
+
const resourceName = resource.name;
|
|
3560
|
+
const basePath = `/${version}/${resourceName}/{id}/${relationName}`;
|
|
3561
|
+
relationConfig.resource;
|
|
3562
|
+
const isToMany = relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany";
|
|
3563
|
+
const paths = {};
|
|
3564
|
+
paths[basePath] = {
|
|
3565
|
+
get: {
|
|
3566
|
+
tags: [resourceName],
|
|
3567
|
+
summary: `Get ${relationName} of ${resourceName}`,
|
|
3568
|
+
description: `Retrieve ${relationName} (${relationConfig.type}) associated with this ${resourceName}. This endpoint uses the RelationPlugin to efficiently load related data` + (relationConfig.partitionHint ? ` via the '${relationConfig.partitionHint}' partition.` : "."),
|
|
3569
|
+
parameters: [
|
|
3570
|
+
{
|
|
3571
|
+
name: "id",
|
|
3572
|
+
in: "path",
|
|
3573
|
+
required: true,
|
|
3574
|
+
description: `${resourceName} ID`,
|
|
3575
|
+
schema: { type: "string" }
|
|
3576
|
+
},
|
|
3577
|
+
...isToMany ? [
|
|
3578
|
+
{
|
|
3579
|
+
name: "limit",
|
|
3580
|
+
in: "query",
|
|
3581
|
+
description: "Maximum number of items to return",
|
|
3582
|
+
schema: { type: "integer", default: 100, minimum: 1, maximum: 1e3 }
|
|
3583
|
+
},
|
|
3584
|
+
{
|
|
3585
|
+
name: "offset",
|
|
3586
|
+
in: "query",
|
|
3587
|
+
description: "Number of items to skip",
|
|
3588
|
+
schema: { type: "integer", default: 0, minimum: 0 }
|
|
3589
|
+
}
|
|
3590
|
+
] : []
|
|
3591
|
+
],
|
|
3592
|
+
responses: {
|
|
3593
|
+
200: {
|
|
3594
|
+
description: "Successful response",
|
|
3595
|
+
content: {
|
|
3596
|
+
"application/json": {
|
|
3597
|
+
schema: isToMany ? {
|
|
3598
|
+
type: "object",
|
|
3599
|
+
properties: {
|
|
3600
|
+
success: { type: "boolean", example: true },
|
|
3601
|
+
data: {
|
|
3602
|
+
type: "array",
|
|
3603
|
+
items: relatedSchema
|
|
3604
|
+
},
|
|
3605
|
+
pagination: {
|
|
3606
|
+
type: "object",
|
|
3607
|
+
properties: {
|
|
3608
|
+
total: { type: "integer" },
|
|
3609
|
+
page: { type: "integer" },
|
|
3610
|
+
pageSize: { type: "integer" },
|
|
3611
|
+
pageCount: { type: "integer" }
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
} : {
|
|
3616
|
+
type: "object",
|
|
3617
|
+
properties: {
|
|
3618
|
+
success: { type: "boolean", example: true },
|
|
3619
|
+
data: relatedSchema
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
},
|
|
3624
|
+
...isToMany ? {
|
|
3625
|
+
headers: {
|
|
3626
|
+
"X-Total-Count": {
|
|
3627
|
+
description: "Total number of related records",
|
|
3628
|
+
schema: { type: "integer" }
|
|
3629
|
+
},
|
|
3630
|
+
"X-Page-Count": {
|
|
3631
|
+
description: "Total number of pages",
|
|
3632
|
+
schema: { type: "integer" }
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
} : {}
|
|
3636
|
+
},
|
|
3637
|
+
404: {
|
|
3638
|
+
description: `${resourceName} not found` + (isToMany ? "" : " or no related resource exists"),
|
|
3639
|
+
content: {
|
|
3640
|
+
"application/json": {
|
|
3641
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
};
|
|
3648
|
+
return paths;
|
|
3649
|
+
}
|
|
3507
3650
|
function generateOpenAPISpec(database, config = {}) {
|
|
3508
3651
|
const {
|
|
3509
3652
|
title = "s3db.js API",
|
|
@@ -3513,12 +3656,33 @@ function generateOpenAPISpec(database, config = {}) {
|
|
|
3513
3656
|
auth = {},
|
|
3514
3657
|
resources: resourceConfigs = {}
|
|
3515
3658
|
} = config;
|
|
3659
|
+
const resourcesTableRows = [];
|
|
3660
|
+
for (const [name, resource] of Object.entries(database.resources)) {
|
|
3661
|
+
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
3662
|
+
continue;
|
|
3663
|
+
}
|
|
3664
|
+
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
3665
|
+
const resourceDescription = resource.config?.description;
|
|
3666
|
+
const descText = typeof resourceDescription === "object" ? resourceDescription.resource : resourceDescription || "No description";
|
|
3667
|
+
resourcesTableRows.push(`| ${name} | ${descText} | \`/${version2}/${name}\` |`);
|
|
3668
|
+
}
|
|
3669
|
+
const enhancedDescription = `${description}
|
|
3670
|
+
|
|
3671
|
+
## Available Resources
|
|
3672
|
+
|
|
3673
|
+
| Resource | Description | Base Path |
|
|
3674
|
+
|----------|-------------|-----------|
|
|
3675
|
+
${resourcesTableRows.join("\n")}
|
|
3676
|
+
|
|
3677
|
+
---
|
|
3678
|
+
|
|
3679
|
+
For detailed information about each endpoint, see the sections below.`;
|
|
3516
3680
|
const spec = {
|
|
3517
3681
|
openapi: "3.1.0",
|
|
3518
3682
|
info: {
|
|
3519
3683
|
title,
|
|
3520
3684
|
version,
|
|
3521
|
-
description,
|
|
3685
|
+
description: enhancedDescription,
|
|
3522
3686
|
contact: {
|
|
3523
3687
|
name: "s3db.js",
|
|
3524
3688
|
url: "https://github.com/forattini-dev/s3db.js"
|
|
@@ -3606,6 +3770,7 @@ function generateOpenAPISpec(database, config = {}) {
|
|
|
3606
3770
|
};
|
|
3607
3771
|
}
|
|
3608
3772
|
const resources = database.resources;
|
|
3773
|
+
const relationsPlugin = database.plugins?.relation || database.plugins?.RelationPlugin || null;
|
|
3609
3774
|
for (const [name, resource] of Object.entries(resources)) {
|
|
3610
3775
|
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
3611
3776
|
continue;
|
|
@@ -3617,11 +3782,38 @@ function generateOpenAPISpec(database, config = {}) {
|
|
|
3617
3782
|
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
3618
3783
|
const paths = generateResourcePaths(resource, version2, config2);
|
|
3619
3784
|
Object.assign(spec.paths, paths);
|
|
3785
|
+
const resourceDescription = resource.config?.description;
|
|
3786
|
+
const tagDescription = typeof resourceDescription === "object" ? resourceDescription.resource : resourceDescription || `Operations for ${name} resource`;
|
|
3620
3787
|
spec.tags.push({
|
|
3621
3788
|
name,
|
|
3622
|
-
description:
|
|
3789
|
+
description: tagDescription
|
|
3623
3790
|
});
|
|
3624
3791
|
spec.components.schemas[name] = generateResourceSchema(resource);
|
|
3792
|
+
if (relationsPlugin && relationsPlugin.relations && relationsPlugin.relations[name]) {
|
|
3793
|
+
const relationsDef = relationsPlugin.relations[name];
|
|
3794
|
+
for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
|
|
3795
|
+
if (relationConfig.type === "belongsTo") {
|
|
3796
|
+
continue;
|
|
3797
|
+
}
|
|
3798
|
+
const exposeRelation = config2?.relations?.[relationName]?.expose !== false;
|
|
3799
|
+
if (!exposeRelation) {
|
|
3800
|
+
continue;
|
|
3801
|
+
}
|
|
3802
|
+
const relatedResource = database.resources[relationConfig.resource];
|
|
3803
|
+
if (!relatedResource) {
|
|
3804
|
+
continue;
|
|
3805
|
+
}
|
|
3806
|
+
const relatedSchema = generateResourceSchema(relatedResource);
|
|
3807
|
+
const relationalPaths = generateRelationalPaths(
|
|
3808
|
+
resource,
|
|
3809
|
+
relationName,
|
|
3810
|
+
relationConfig,
|
|
3811
|
+
version2,
|
|
3812
|
+
relatedSchema
|
|
3813
|
+
);
|
|
3814
|
+
Object.assign(spec.paths, relationalPaths);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3625
3817
|
}
|
|
3626
3818
|
if (auth.jwt?.enabled || auth.apiKey?.enabled || auth.basic?.enabled) {
|
|
3627
3819
|
spec.paths["/auth/login"] = {
|
|
@@ -3935,6 +4127,7 @@ class ApiServer {
|
|
|
3935
4127
|
this.server = null;
|
|
3936
4128
|
this.isRunning = false;
|
|
3937
4129
|
this.openAPISpec = null;
|
|
4130
|
+
this.relationsPlugin = this.options.database?.plugins?.relation || this.options.database?.plugins?.RelationPlugin || null;
|
|
3938
4131
|
this._setupRoutes();
|
|
3939
4132
|
}
|
|
3940
4133
|
/**
|
|
@@ -4045,6 +4238,9 @@ class ApiServer {
|
|
|
4045
4238
|
}
|
|
4046
4239
|
}
|
|
4047
4240
|
this._setupResourceRoutes();
|
|
4241
|
+
if (this.relationsPlugin) {
|
|
4242
|
+
this._setupRelationalRoutes();
|
|
4243
|
+
}
|
|
4048
4244
|
this.app.onError((err, c) => {
|
|
4049
4245
|
return errorHandler(err, c);
|
|
4050
4246
|
});
|
|
@@ -4085,6 +4281,53 @@ class ApiServer {
|
|
|
4085
4281
|
}
|
|
4086
4282
|
}
|
|
4087
4283
|
}
|
|
4284
|
+
/**
|
|
4285
|
+
* Setup relational routes (when RelationPlugin is active)
|
|
4286
|
+
* @private
|
|
4287
|
+
*/
|
|
4288
|
+
_setupRelationalRoutes() {
|
|
4289
|
+
if (!this.relationsPlugin || !this.relationsPlugin.relations) {
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
const { database } = this.options;
|
|
4293
|
+
const relations = this.relationsPlugin.relations;
|
|
4294
|
+
if (this.options.verbose) {
|
|
4295
|
+
console.log("[API Plugin] Setting up relational routes...");
|
|
4296
|
+
}
|
|
4297
|
+
for (const [resourceName, relationsDef] of Object.entries(relations)) {
|
|
4298
|
+
const resource = database.resources[resourceName];
|
|
4299
|
+
if (!resource) {
|
|
4300
|
+
if (this.options.verbose) {
|
|
4301
|
+
console.warn(`[API Plugin] Resource '${resourceName}' not found for relational routes`);
|
|
4302
|
+
}
|
|
4303
|
+
continue;
|
|
4304
|
+
}
|
|
4305
|
+
if (resourceName.startsWith("plg_") && !this.options.resources[resourceName]) {
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
const version = resource.config?.currentVersion || resource.version || "v1";
|
|
4309
|
+
for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
|
|
4310
|
+
if (relationConfig.type === "belongsTo") {
|
|
4311
|
+
continue;
|
|
4312
|
+
}
|
|
4313
|
+
const resourceConfig = this.options.resources[resourceName];
|
|
4314
|
+
const exposeRelation = resourceConfig?.relations?.[relationName]?.expose !== false;
|
|
4315
|
+
if (!exposeRelation) {
|
|
4316
|
+
continue;
|
|
4317
|
+
}
|
|
4318
|
+
const relationalApp = createRelationalRoutes(
|
|
4319
|
+
resource,
|
|
4320
|
+
relationName,
|
|
4321
|
+
relationConfig);
|
|
4322
|
+
this.app.route(`/${version}/${resourceName}/:id/${relationName}`, relationalApp);
|
|
4323
|
+
if (this.options.verbose) {
|
|
4324
|
+
console.log(
|
|
4325
|
+
`[API Plugin] Mounted relational route: /${version}/${resourceName}/:id/${relationName} (${relationConfig.type} -> ${relationConfig.resource})`
|
|
4326
|
+
);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4088
4331
|
/**
|
|
4089
4332
|
* Start the server
|
|
4090
4333
|
* @returns {Promise<void>}
|
|
@@ -4173,81 +4416,97 @@ class ApiServer {
|
|
|
4173
4416
|
const PLUGIN_DEPENDENCIES = {
|
|
4174
4417
|
"postgresql-replicator": {
|
|
4175
4418
|
name: "PostgreSQL Replicator",
|
|
4419
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4176
4420
|
dependencies: {
|
|
4177
4421
|
"pg": {
|
|
4178
4422
|
version: "^8.0.0",
|
|
4179
4423
|
description: "PostgreSQL client for Node.js",
|
|
4180
|
-
installCommand: "pnpm add pg"
|
|
4424
|
+
installCommand: "pnpm add pg",
|
|
4425
|
+
npmUrl: "https://www.npmjs.com/package/pg"
|
|
4181
4426
|
}
|
|
4182
4427
|
}
|
|
4183
4428
|
},
|
|
4184
4429
|
"bigquery-replicator": {
|
|
4185
4430
|
name: "BigQuery Replicator",
|
|
4431
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4186
4432
|
dependencies: {
|
|
4187
4433
|
"@google-cloud/bigquery": {
|
|
4188
4434
|
version: "^7.0.0",
|
|
4189
4435
|
description: "Google Cloud BigQuery SDK",
|
|
4190
|
-
installCommand: "pnpm add @google-cloud/bigquery"
|
|
4436
|
+
installCommand: "pnpm add @google-cloud/bigquery",
|
|
4437
|
+
npmUrl: "https://www.npmjs.com/package/@google-cloud/bigquery"
|
|
4191
4438
|
}
|
|
4192
4439
|
}
|
|
4193
4440
|
},
|
|
4194
4441
|
"sqs-replicator": {
|
|
4195
4442
|
name: "SQS Replicator",
|
|
4443
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4196
4444
|
dependencies: {
|
|
4197
4445
|
"@aws-sdk/client-sqs": {
|
|
4198
4446
|
version: "^3.0.0",
|
|
4199
4447
|
description: "AWS SDK for SQS",
|
|
4200
|
-
installCommand: "pnpm add @aws-sdk/client-sqs"
|
|
4448
|
+
installCommand: "pnpm add @aws-sdk/client-sqs",
|
|
4449
|
+
npmUrl: "https://www.npmjs.com/package/@aws-sdk/client-sqs"
|
|
4201
4450
|
}
|
|
4202
4451
|
}
|
|
4203
4452
|
},
|
|
4204
4453
|
"sqs-consumer": {
|
|
4205
4454
|
name: "SQS Queue Consumer",
|
|
4455
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md",
|
|
4206
4456
|
dependencies: {
|
|
4207
4457
|
"@aws-sdk/client-sqs": {
|
|
4208
4458
|
version: "^3.0.0",
|
|
4209
4459
|
description: "AWS SDK for SQS",
|
|
4210
|
-
installCommand: "pnpm add @aws-sdk/client-sqs"
|
|
4460
|
+
installCommand: "pnpm add @aws-sdk/client-sqs",
|
|
4461
|
+
npmUrl: "https://www.npmjs.com/package/@aws-sdk/client-sqs"
|
|
4211
4462
|
}
|
|
4212
4463
|
}
|
|
4213
4464
|
},
|
|
4214
4465
|
"rabbitmq-consumer": {
|
|
4215
4466
|
name: "RabbitMQ Queue Consumer",
|
|
4467
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md",
|
|
4216
4468
|
dependencies: {
|
|
4217
4469
|
"amqplib": {
|
|
4218
4470
|
version: "^0.10.0",
|
|
4219
4471
|
description: "AMQP 0-9-1 library for RabbitMQ",
|
|
4220
|
-
installCommand: "pnpm add amqplib"
|
|
4472
|
+
installCommand: "pnpm add amqplib",
|
|
4473
|
+
npmUrl: "https://www.npmjs.com/package/amqplib"
|
|
4221
4474
|
}
|
|
4222
4475
|
}
|
|
4223
4476
|
},
|
|
4224
4477
|
"tfstate-plugin": {
|
|
4225
|
-
name: "
|
|
4478
|
+
name: "Tfstate Plugin",
|
|
4479
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/tfstate.md",
|
|
4226
4480
|
dependencies: {
|
|
4227
4481
|
"node-cron": {
|
|
4228
4482
|
version: "^4.0.0",
|
|
4229
4483
|
description: "Cron job scheduler for auto-sync functionality",
|
|
4230
|
-
installCommand: "pnpm add node-cron"
|
|
4484
|
+
installCommand: "pnpm add node-cron",
|
|
4485
|
+
npmUrl: "https://www.npmjs.com/package/node-cron"
|
|
4231
4486
|
}
|
|
4232
4487
|
}
|
|
4233
4488
|
},
|
|
4234
4489
|
"api-plugin": {
|
|
4235
4490
|
name: "API Plugin",
|
|
4491
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/api.md",
|
|
4236
4492
|
dependencies: {
|
|
4237
4493
|
"hono": {
|
|
4238
4494
|
version: "^4.0.0",
|
|
4239
4495
|
description: "Ultra-light HTTP server framework",
|
|
4240
|
-
installCommand: "pnpm add hono"
|
|
4496
|
+
installCommand: "pnpm add hono",
|
|
4497
|
+
npmUrl: "https://www.npmjs.com/package/hono"
|
|
4241
4498
|
},
|
|
4242
4499
|
"@hono/node-server": {
|
|
4243
4500
|
version: "^1.0.0",
|
|
4244
4501
|
description: "Node.js adapter for Hono",
|
|
4245
|
-
installCommand: "pnpm add @hono/node-server"
|
|
4502
|
+
installCommand: "pnpm add @hono/node-server",
|
|
4503
|
+
npmUrl: "https://www.npmjs.com/package/@hono/node-server"
|
|
4246
4504
|
},
|
|
4247
4505
|
"@hono/swagger-ui": {
|
|
4248
4506
|
version: "^0.4.0",
|
|
4249
4507
|
description: "Swagger UI integration for Hono",
|
|
4250
|
-
installCommand: "pnpm add @hono/swagger-ui"
|
|
4508
|
+
installCommand: "pnpm add @hono/swagger-ui",
|
|
4509
|
+
npmUrl: "https://www.npmjs.com/package/@hono/swagger-ui"
|
|
4251
4510
|
}
|
|
4252
4511
|
}
|
|
4253
4512
|
}
|
|
@@ -4333,21 +4592,55 @@ async function requirePluginDependency(pluginId, options = {}) {
|
|
|
4333
4592
|
}
|
|
4334
4593
|
const valid = missing.length === 0 && incompatible.length === 0;
|
|
4335
4594
|
if (!valid && throwOnError) {
|
|
4595
|
+
const depCount = Object.keys(pluginDef.dependencies).length;
|
|
4596
|
+
const missingCount = missing.length;
|
|
4597
|
+
const incompatCount = incompatible.length;
|
|
4336
4598
|
const errorMsg = [
|
|
4337
|
-
`
|
|
4338
|
-
${pluginDef.name} - Missing dependencies detected!
|
|
4339
|
-
`,
|
|
4340
|
-
`Plugin ID: ${pluginId}`,
|
|
4341
4599
|
"",
|
|
4600
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
4601
|
+
`\u2551 \u274C ${pluginDef.name} - Missing Dependencies \u2551`,
|
|
4602
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
4603
|
+
"",
|
|
4604
|
+
`\u{1F4E6} Plugin: ${pluginId}`,
|
|
4605
|
+
`\u{1F4CA} Status: ${depCount - missingCount - incompatCount}/${depCount} dependencies satisfied`,
|
|
4606
|
+
"",
|
|
4607
|
+
"\u{1F50D} Dependency Status:",
|
|
4608
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
4342
4609
|
...messages,
|
|
4343
4610
|
"",
|
|
4344
|
-
"Quick
|
|
4345
|
-
|
|
4611
|
+
"\u{1F680} Quick Fix - Install Missing Dependencies:",
|
|
4612
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
4613
|
+
"",
|
|
4614
|
+
" Option 1: Install individually",
|
|
4615
|
+
...Object.entries(pluginDef.dependencies).filter(([pkg]) => missing.includes(pkg) || incompatible.includes(pkg)).map(([pkg, info]) => ` ${info.installCommand}`),
|
|
4616
|
+
"",
|
|
4617
|
+
" Option 2: Install all at once",
|
|
4618
|
+
` pnpm add ${Object.keys(pluginDef.dependencies).join(" ")}`,
|
|
4619
|
+
"",
|
|
4620
|
+
"\u{1F4DA} Documentation:",
|
|
4621
|
+
` ${pluginDef.docsUrl}`,
|
|
4622
|
+
"",
|
|
4623
|
+
"\u{1F4A1} Troubleshooting:",
|
|
4624
|
+
" \u2022 If packages are installed but not detected, try:",
|
|
4625
|
+
" 1. Delete node_modules and reinstall: rm -rf node_modules && pnpm install",
|
|
4626
|
+
" 2. Check Node.js version: node --version (requires Node 18+)",
|
|
4627
|
+
" 3. Verify pnpm version: pnpm --version (requires pnpm 8+)",
|
|
4346
4628
|
"",
|
|
4347
|
-
"
|
|
4348
|
-
|
|
4629
|
+
" \u2022 Still having issues? Check:",
|
|
4630
|
+
" - Package.json has correct dependencies listed",
|
|
4631
|
+
" - No conflicting versions in pnpm-lock.yaml",
|
|
4632
|
+
" - File permissions (especially in node_modules/)",
|
|
4633
|
+
"",
|
|
4634
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
4635
|
+
""
|
|
4349
4636
|
].join("\n");
|
|
4350
|
-
|
|
4637
|
+
const error = new Error(errorMsg);
|
|
4638
|
+
error.pluginId = pluginId;
|
|
4639
|
+
error.pluginName = pluginDef.name;
|
|
4640
|
+
error.missing = missing;
|
|
4641
|
+
error.incompatible = incompatible;
|
|
4642
|
+
error.docsUrl = pluginDef.docsUrl;
|
|
4643
|
+
throw error;
|
|
4351
4644
|
}
|
|
4352
4645
|
return { valid, missing, incompatible, messages };
|
|
4353
4646
|
}
|
|
@@ -4364,7 +4657,6 @@ class ApiPlugin extends Plugin {
|
|
|
4364
4657
|
port: options.port || 3e3,
|
|
4365
4658
|
host: options.host || "0.0.0.0",
|
|
4366
4659
|
verbose: options.verbose || false,
|
|
4367
|
-
// API Documentation (supports both new and legacy formats)
|
|
4368
4660
|
docs: {
|
|
4369
4661
|
enabled: options.docs?.enabled !== false && options.docsEnabled !== false,
|
|
4370
4662
|
// Enable by default
|
|
@@ -5743,8 +6035,6 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
5743
6035
|
strategy: "all",
|
|
5744
6036
|
// 'all', 'any', 'priority'
|
|
5745
6037
|
concurrency: 3,
|
|
5746
|
-
requireAll: true,
|
|
5747
|
-
// For backward compatibility
|
|
5748
6038
|
...config
|
|
5749
6039
|
});
|
|
5750
6040
|
this.drivers = [];
|
|
@@ -6381,13 +6671,13 @@ class BackupPlugin extends Plugin {
|
|
|
6381
6671
|
createdAt: now.toISOString().slice(0, 10)
|
|
6382
6672
|
};
|
|
6383
6673
|
const [ok] = await tryFn(
|
|
6384
|
-
() => this.database.
|
|
6674
|
+
() => this.database.resources[this.config.backupMetadataResource].insert(metadata)
|
|
6385
6675
|
);
|
|
6386
6676
|
return metadata;
|
|
6387
6677
|
}
|
|
6388
6678
|
async _updateBackupMetadata(backupId, updates) {
|
|
6389
6679
|
const [ok] = await tryFn(
|
|
6390
|
-
() => this.database.
|
|
6680
|
+
() => this.database.resources[this.config.backupMetadataResource].update(backupId, updates)
|
|
6391
6681
|
);
|
|
6392
6682
|
}
|
|
6393
6683
|
async _createBackupManifest(type, options) {
|
|
@@ -6422,7 +6712,7 @@ class BackupPlugin extends Plugin {
|
|
|
6422
6712
|
let sinceTimestamp = null;
|
|
6423
6713
|
if (type === "incremental") {
|
|
6424
6714
|
const [lastBackupOk, , lastBackups] = await tryFn(
|
|
6425
|
-
() => this.database.
|
|
6715
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6426
6716
|
filter: {
|
|
6427
6717
|
status: "completed",
|
|
6428
6718
|
type: { $in: ["full", "incremental"] }
|
|
@@ -6730,7 +7020,7 @@ class BackupPlugin extends Plugin {
|
|
|
6730
7020
|
try {
|
|
6731
7021
|
const driverBackups = await this.driver.list(options);
|
|
6732
7022
|
const [metaOk, , metadataRecords] = await tryFn(
|
|
6733
|
-
() => this.database.
|
|
7023
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6734
7024
|
limit: options.limit || 50,
|
|
6735
7025
|
sort: { timestamp: -1 }
|
|
6736
7026
|
})
|
|
@@ -6758,14 +7048,14 @@ class BackupPlugin extends Plugin {
|
|
|
6758
7048
|
*/
|
|
6759
7049
|
async getBackupStatus(backupId) {
|
|
6760
7050
|
const [ok, , backup] = await tryFn(
|
|
6761
|
-
() => this.database.
|
|
7051
|
+
() => this.database.resources[this.config.backupMetadataResource].get(backupId)
|
|
6762
7052
|
);
|
|
6763
7053
|
return ok ? backup : null;
|
|
6764
7054
|
}
|
|
6765
7055
|
async _cleanupOldBackups() {
|
|
6766
7056
|
try {
|
|
6767
7057
|
const [listOk, , allBackups] = await tryFn(
|
|
6768
|
-
() => this.database.
|
|
7058
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6769
7059
|
filter: { status: "completed" },
|
|
6770
7060
|
sort: { timestamp: -1 }
|
|
6771
7061
|
})
|
|
@@ -6832,7 +7122,7 @@ class BackupPlugin extends Plugin {
|
|
|
6832
7122
|
for (const backup of backupsToDelete) {
|
|
6833
7123
|
try {
|
|
6834
7124
|
await this.driver.delete(backup.id, backup.driverInfo);
|
|
6835
|
-
await this.database.
|
|
7125
|
+
await this.database.resources[this.config.backupMetadataResource].delete(backup.id);
|
|
6836
7126
|
if (this.config.verbose) {
|
|
6837
7127
|
console.log(`[BackupPlugin] Deleted old backup: ${backup.id}`);
|
|
6838
7128
|
}
|
|
@@ -6868,12 +7158,6 @@ class BackupPlugin extends Plugin {
|
|
|
6868
7158
|
await this.driver.cleanup();
|
|
6869
7159
|
}
|
|
6870
7160
|
}
|
|
6871
|
-
/**
|
|
6872
|
-
* Cleanup plugin resources (alias for stop for backward compatibility)
|
|
6873
|
-
*/
|
|
6874
|
-
async cleanup() {
|
|
6875
|
-
await this.stop();
|
|
6876
|
-
}
|
|
6877
7161
|
}
|
|
6878
7162
|
|
|
6879
7163
|
class CacheError extends S3dbError {
|
|
@@ -9769,9 +10053,6 @@ async function consolidateRecord(originalId, transactionResource, targetResource
|
|
|
9769
10053
|
if (txnWithCohorts.cohortMonth && !txn.cohortMonth) {
|
|
9770
10054
|
updateData.cohortMonth = txnWithCohorts.cohortMonth;
|
|
9771
10055
|
}
|
|
9772
|
-
if (txn.value === null || txn.value === void 0) {
|
|
9773
|
-
updateData.value = 1;
|
|
9774
|
-
}
|
|
9775
10056
|
const [ok2, err2] = await tryFn(
|
|
9776
10057
|
() => transactionResource.update(txn.id, updateData)
|
|
9777
10058
|
);
|
|
@@ -11095,8 +11376,7 @@ async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
11095
11376
|
operation: "string|required",
|
|
11096
11377
|
timestamp: "string|required",
|
|
11097
11378
|
cohortDate: "string|required",
|
|
11098
|
-
cohortHour: "string|
|
|
11099
|
-
// ✅ FIX BUG #2: Changed from required to optional for migration compatibility
|
|
11379
|
+
cohortHour: "string|required",
|
|
11100
11380
|
cohortWeek: "string|optional",
|
|
11101
11381
|
cohortMonth: "string|optional",
|
|
11102
11382
|
source: "string|optional",
|
|
@@ -13430,7 +13710,7 @@ class RelationPlugin extends Plugin {
|
|
|
13430
13710
|
* @private
|
|
13431
13711
|
*/
|
|
13432
13712
|
async _setupResourceRelations(resourceName, relationsDef) {
|
|
13433
|
-
const resource = this.database.
|
|
13713
|
+
const resource = this.database.resources[resourceName];
|
|
13434
13714
|
if (!resource) {
|
|
13435
13715
|
if (this.verbose) {
|
|
13436
13716
|
console.warn(`[RelationPlugin] Resource "${resourceName}" not found, will setup when created`);
|
|
@@ -13541,7 +13821,7 @@ class RelationPlugin extends Plugin {
|
|
|
13541
13821
|
for (const record of records) {
|
|
13542
13822
|
const relatedData = record[relationName];
|
|
13543
13823
|
if (relatedData) {
|
|
13544
|
-
const relatedResource = this.database.
|
|
13824
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13545
13825
|
const relatedArray = Array.isArray(relatedData) ? relatedData : [relatedData];
|
|
13546
13826
|
if (relatedArray.length > 0) {
|
|
13547
13827
|
await this._eagerLoad(relatedArray, nestedIncludes, relatedResource);
|
|
@@ -13592,7 +13872,7 @@ class RelationPlugin extends Plugin {
|
|
|
13592
13872
|
* @private
|
|
13593
13873
|
*/
|
|
13594
13874
|
async _loadHasOne(records, relationName, config, sourceResource) {
|
|
13595
|
-
const relatedResource = this.database.
|
|
13875
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13596
13876
|
if (!relatedResource) {
|
|
13597
13877
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13598
13878
|
sourceResource: sourceResource.name,
|
|
@@ -13631,7 +13911,7 @@ class RelationPlugin extends Plugin {
|
|
|
13631
13911
|
* @private
|
|
13632
13912
|
*/
|
|
13633
13913
|
async _loadHasMany(records, relationName, config, sourceResource) {
|
|
13634
|
-
const relatedResource = this.database.
|
|
13914
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13635
13915
|
if (!relatedResource) {
|
|
13636
13916
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13637
13917
|
sourceResource: sourceResource.name,
|
|
@@ -13677,7 +13957,7 @@ class RelationPlugin extends Plugin {
|
|
|
13677
13957
|
* @private
|
|
13678
13958
|
*/
|
|
13679
13959
|
async _loadBelongsTo(records, relationName, config, sourceResource) {
|
|
13680
|
-
const relatedResource = this.database.
|
|
13960
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13681
13961
|
if (!relatedResource) {
|
|
13682
13962
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13683
13963
|
sourceResource: sourceResource.name,
|
|
@@ -13727,14 +14007,14 @@ class RelationPlugin extends Plugin {
|
|
|
13727
14007
|
* @private
|
|
13728
14008
|
*/
|
|
13729
14009
|
async _loadBelongsToMany(records, relationName, config, sourceResource) {
|
|
13730
|
-
const relatedResource = this.database.
|
|
14010
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13731
14011
|
if (!relatedResource) {
|
|
13732
14012
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13733
14013
|
sourceResource: sourceResource.name,
|
|
13734
14014
|
relation: relationName
|
|
13735
14015
|
});
|
|
13736
14016
|
}
|
|
13737
|
-
const junctionResource = this.database.
|
|
14017
|
+
const junctionResource = this.database.resources[config.through];
|
|
13738
14018
|
if (!junctionResource) {
|
|
13739
14019
|
throw new JunctionTableNotFoundError(config.through, {
|
|
13740
14020
|
sourceResource: sourceResource.name,
|
|
@@ -13918,7 +14198,7 @@ class RelationPlugin extends Plugin {
|
|
|
13918
14198
|
*/
|
|
13919
14199
|
async _cascadeDelete(record, resource, relationName, config) {
|
|
13920
14200
|
this.stats.cascadeOperations++;
|
|
13921
|
-
const relatedResource = this.database.
|
|
14201
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13922
14202
|
if (!relatedResource) {
|
|
13923
14203
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13924
14204
|
sourceResource: resource.name,
|
|
@@ -13926,7 +14206,7 @@ class RelationPlugin extends Plugin {
|
|
|
13926
14206
|
});
|
|
13927
14207
|
}
|
|
13928
14208
|
const deletedRecords = [];
|
|
13929
|
-
config.type === "belongsToMany" ? this.database.
|
|
14209
|
+
config.type === "belongsToMany" ? this.database.resources[config.through] : null;
|
|
13930
14210
|
try {
|
|
13931
14211
|
if (config.type === "hasMany") {
|
|
13932
14212
|
let relatedRecords;
|
|
@@ -13977,7 +14257,7 @@ class RelationPlugin extends Plugin {
|
|
|
13977
14257
|
await relatedResource.delete(relatedRecords[0].id);
|
|
13978
14258
|
}
|
|
13979
14259
|
} else if (config.type === "belongsToMany") {
|
|
13980
|
-
const junctionResource2 = this.database.
|
|
14260
|
+
const junctionResource2 = this.database.resources[config.through];
|
|
13981
14261
|
if (junctionResource2) {
|
|
13982
14262
|
let junctionRecords;
|
|
13983
14263
|
const partitionName = this._findPartitionByField(junctionResource2, config.foreignKey);
|
|
@@ -14045,7 +14325,7 @@ class RelationPlugin extends Plugin {
|
|
|
14045
14325
|
*/
|
|
14046
14326
|
async _cascadeUpdate(record, changes, resource, relationName, config) {
|
|
14047
14327
|
this.stats.cascadeOperations++;
|
|
14048
|
-
const relatedResource = this.database.
|
|
14328
|
+
const relatedResource = this.database.resources[config.resource];
|
|
14049
14329
|
if (!relatedResource) {
|
|
14050
14330
|
return;
|
|
14051
14331
|
}
|
|
@@ -19330,12 +19610,42 @@ ${errorDetails}`,
|
|
|
19330
19610
|
createdBy
|
|
19331
19611
|
};
|
|
19332
19612
|
this.hooks = {
|
|
19613
|
+
// Insert hooks
|
|
19333
19614
|
beforeInsert: [],
|
|
19334
19615
|
afterInsert: [],
|
|
19616
|
+
// Update hooks
|
|
19335
19617
|
beforeUpdate: [],
|
|
19336
19618
|
afterUpdate: [],
|
|
19619
|
+
// Delete hooks
|
|
19337
19620
|
beforeDelete: [],
|
|
19338
|
-
afterDelete: []
|
|
19621
|
+
afterDelete: [],
|
|
19622
|
+
// Get hooks
|
|
19623
|
+
beforeGet: [],
|
|
19624
|
+
afterGet: [],
|
|
19625
|
+
// List hooks
|
|
19626
|
+
beforeList: [],
|
|
19627
|
+
afterList: [],
|
|
19628
|
+
// Query hooks
|
|
19629
|
+
beforeQuery: [],
|
|
19630
|
+
afterQuery: [],
|
|
19631
|
+
// Patch hooks
|
|
19632
|
+
beforePatch: [],
|
|
19633
|
+
afterPatch: [],
|
|
19634
|
+
// Replace hooks
|
|
19635
|
+
beforeReplace: [],
|
|
19636
|
+
afterReplace: [],
|
|
19637
|
+
// Exists hooks
|
|
19638
|
+
beforeExists: [],
|
|
19639
|
+
afterExists: [],
|
|
19640
|
+
// Count hooks
|
|
19641
|
+
beforeCount: [],
|
|
19642
|
+
afterCount: [],
|
|
19643
|
+
// GetMany hooks
|
|
19644
|
+
beforeGetMany: [],
|
|
19645
|
+
afterGetMany: [],
|
|
19646
|
+
// DeleteMany hooks
|
|
19647
|
+
beforeDeleteMany: [],
|
|
19648
|
+
afterDeleteMany: []
|
|
19339
19649
|
};
|
|
19340
19650
|
this.attributes = attributes || {};
|
|
19341
19651
|
this.map = config.map;
|
|
@@ -19398,19 +19708,6 @@ ${errorDetails}`,
|
|
|
19398
19708
|
}
|
|
19399
19709
|
return idSize;
|
|
19400
19710
|
}
|
|
19401
|
-
/**
|
|
19402
|
-
* Get resource options (for backward compatibility with tests)
|
|
19403
|
-
*/
|
|
19404
|
-
get options() {
|
|
19405
|
-
return {
|
|
19406
|
-
timestamps: this.config.timestamps,
|
|
19407
|
-
partitions: this.config.partitions || {},
|
|
19408
|
-
cache: this.config.cache,
|
|
19409
|
-
autoDecrypt: this.config.autoDecrypt,
|
|
19410
|
-
paranoid: this.config.paranoid,
|
|
19411
|
-
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
19412
|
-
};
|
|
19413
|
-
}
|
|
19414
19711
|
export() {
|
|
19415
19712
|
const exported = this.schema.export();
|
|
19416
19713
|
exported.behavior = this.behavior;
|
|
@@ -19537,19 +19834,71 @@ ${errorDetails}`,
|
|
|
19537
19834
|
return data;
|
|
19538
19835
|
});
|
|
19539
19836
|
}
|
|
19540
|
-
|
|
19837
|
+
/**
|
|
19838
|
+
* Validate data against resource schema without saving
|
|
19839
|
+
* @param {Object} data - Data to validate
|
|
19840
|
+
* @param {Object} options - Validation options
|
|
19841
|
+
* @param {boolean} options.throwOnError - Throw error if validation fails (default: false)
|
|
19842
|
+
* @param {boolean} options.includeId - Include ID validation (default: false)
|
|
19843
|
+
* @param {boolean} options.mutateOriginal - Allow mutation of original data (default: false)
|
|
19844
|
+
* @returns {Promise<{valid: boolean, isValid: boolean, errors: Array, data: Object, original: Object}>} Validation result
|
|
19845
|
+
* @example
|
|
19846
|
+
* // Validate before insert
|
|
19847
|
+
* const result = await resource.validate({
|
|
19848
|
+
* name: 'John Doe',
|
|
19849
|
+
* email: 'invalid-email' // Will fail email validation
|
|
19850
|
+
* });
|
|
19851
|
+
*
|
|
19852
|
+
* if (!result.valid) {
|
|
19853
|
+
* console.log('Validation errors:', result.errors);
|
|
19854
|
+
* // [{ field: 'email', message: '...', ... }]
|
|
19855
|
+
* }
|
|
19856
|
+
*
|
|
19857
|
+
* // Throw on error
|
|
19858
|
+
* try {
|
|
19859
|
+
* await resource.validate({ email: 'bad' }, { throwOnError: true });
|
|
19860
|
+
* } catch (err) {
|
|
19861
|
+
* console.log('Validation failed:', err.message);
|
|
19862
|
+
* }
|
|
19863
|
+
*/
|
|
19864
|
+
async validate(data, options = {}) {
|
|
19865
|
+
const {
|
|
19866
|
+
throwOnError = false,
|
|
19867
|
+
includeId = false,
|
|
19868
|
+
mutateOriginal = false
|
|
19869
|
+
} = options;
|
|
19870
|
+
const dataToValidate = mutateOriginal ? data : cloneDeep(data);
|
|
19871
|
+
if (!includeId && dataToValidate.id) {
|
|
19872
|
+
delete dataToValidate.id;
|
|
19873
|
+
}
|
|
19541
19874
|
const result = {
|
|
19542
19875
|
original: cloneDeep(data),
|
|
19543
19876
|
isValid: false,
|
|
19544
|
-
errors: []
|
|
19877
|
+
errors: [],
|
|
19878
|
+
data: dataToValidate
|
|
19545
19879
|
};
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19880
|
+
try {
|
|
19881
|
+
const check = await this.schema.validate(dataToValidate, { mutateOriginal });
|
|
19882
|
+
if (check === true) {
|
|
19883
|
+
result.isValid = true;
|
|
19884
|
+
} else {
|
|
19885
|
+
result.errors = Array.isArray(check) ? check : [check];
|
|
19886
|
+
result.isValid = false;
|
|
19887
|
+
if (throwOnError) {
|
|
19888
|
+
const error = new Error("Validation failed");
|
|
19889
|
+
error.validationErrors = result.errors;
|
|
19890
|
+
error.invalidData = data;
|
|
19891
|
+
throw error;
|
|
19892
|
+
}
|
|
19893
|
+
}
|
|
19894
|
+
} catch (err) {
|
|
19895
|
+
if (!throwOnError) {
|
|
19896
|
+
result.errors = [{ message: err.message, error: err }];
|
|
19897
|
+
result.isValid = false;
|
|
19898
|
+
} else {
|
|
19899
|
+
throw err;
|
|
19900
|
+
}
|
|
19551
19901
|
}
|
|
19552
|
-
result.data = data;
|
|
19553
19902
|
return result;
|
|
19554
19903
|
}
|
|
19555
19904
|
/**
|
|
@@ -19814,12 +20163,12 @@ ${errorDetails}`,
|
|
|
19814
20163
|
const exists = await this.exists(id$1);
|
|
19815
20164
|
if (exists) throw new Error(`Resource with id '${id$1}' already exists`);
|
|
19816
20165
|
this.getResourceKey(id$1 || "(auto)");
|
|
19817
|
-
if (this.
|
|
20166
|
+
if (this.config.timestamps) {
|
|
19818
20167
|
attributes.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19819
20168
|
attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19820
20169
|
}
|
|
19821
20170
|
const attributesWithDefaults = this.applyDefaults(attributes);
|
|
19822
|
-
const completeData = { id: id$1, ...attributesWithDefaults };
|
|
20171
|
+
const completeData = id$1 !== void 0 ? { id: id$1, ...attributesWithDefaults } : { ...attributesWithDefaults };
|
|
19823
20172
|
const preProcessedData = await this.executeHooks("beforeInsert", completeData);
|
|
19824
20173
|
const extraProps = Object.keys(preProcessedData).filter(
|
|
19825
20174
|
(k) => !(k in completeData) || preProcessedData[k] !== completeData[k]
|
|
@@ -19830,7 +20179,7 @@ ${errorDetails}`,
|
|
|
19830
20179
|
errors,
|
|
19831
20180
|
isValid,
|
|
19832
20181
|
data: validated
|
|
19833
|
-
} = await this.validate(preProcessedData);
|
|
20182
|
+
} = await this.validate(preProcessedData, { includeId: true });
|
|
19834
20183
|
if (!isValid) {
|
|
19835
20184
|
const errorMsg = errors && errors.length && errors[0].message ? errors[0].message : "Insert failed";
|
|
19836
20185
|
throw new InvalidResourceItem({
|
|
@@ -19934,6 +20283,7 @@ ${errorDetails}`,
|
|
|
19934
20283
|
async get(id) {
|
|
19935
20284
|
if (isObject(id)) throw new Error(`id cannot be an object`);
|
|
19936
20285
|
if (isEmpty(id)) throw new Error("id cannot be empty");
|
|
20286
|
+
await this.executeHooks("beforeGet", { id });
|
|
19937
20287
|
const key = this.getResourceKey(id);
|
|
19938
20288
|
const [ok, err, request] = await tryFn(() => this.client.getObject(key));
|
|
19939
20289
|
if (!ok) {
|
|
@@ -19982,17 +20332,67 @@ ${errorDetails}`,
|
|
|
19982
20332
|
if (objectVersion !== this.version) {
|
|
19983
20333
|
data = await this.applyVersionMapping(data, objectVersion, this.version);
|
|
19984
20334
|
}
|
|
20335
|
+
data = await this.executeHooks("afterGet", data);
|
|
19985
20336
|
this.emit("get", data);
|
|
19986
20337
|
const value = data;
|
|
19987
20338
|
return value;
|
|
19988
20339
|
}
|
|
20340
|
+
/**
|
|
20341
|
+
* Retrieve a resource object by ID, or return null if not found
|
|
20342
|
+
* @param {string} id - Resource ID
|
|
20343
|
+
* @returns {Promise<Object|null>} The resource object or null if not found
|
|
20344
|
+
* @example
|
|
20345
|
+
* const user = await resource.getOrNull('user-123');
|
|
20346
|
+
* if (user) {
|
|
20347
|
+
* console.log('Found user:', user.name);
|
|
20348
|
+
* } else {
|
|
20349
|
+
* console.log('User not found');
|
|
20350
|
+
* }
|
|
20351
|
+
*/
|
|
20352
|
+
async getOrNull(id) {
|
|
20353
|
+
const [ok, err, data] = await tryFn(() => this.get(id));
|
|
20354
|
+
if (!ok && err && (err.name === "NoSuchKey" || err.message?.includes("NoSuchKey"))) {
|
|
20355
|
+
return null;
|
|
20356
|
+
}
|
|
20357
|
+
if (!ok) {
|
|
20358
|
+
throw err;
|
|
20359
|
+
}
|
|
20360
|
+
return data;
|
|
20361
|
+
}
|
|
20362
|
+
/**
|
|
20363
|
+
* Retrieve a resource object by ID, or throw ResourceNotFoundError if not found
|
|
20364
|
+
* @param {string} id - Resource ID
|
|
20365
|
+
* @returns {Promise<Object>} The resource object
|
|
20366
|
+
* @throws {ResourceError} If resource does not exist
|
|
20367
|
+
* @example
|
|
20368
|
+
* // Throws error if user doesn't exist (no need for null check)
|
|
20369
|
+
* const user = await resource.getOrThrow('user-123');
|
|
20370
|
+
* console.log('User name:', user.name); // Safe to access
|
|
20371
|
+
*/
|
|
20372
|
+
async getOrThrow(id) {
|
|
20373
|
+
const [ok, err, data] = await tryFn(() => this.get(id));
|
|
20374
|
+
if (!ok && err && (err.name === "NoSuchKey" || err.message?.includes("NoSuchKey"))) {
|
|
20375
|
+
throw new ResourceError(`Resource '${this.name}' with id '${id}' not found`, {
|
|
20376
|
+
resourceName: this.name,
|
|
20377
|
+
operation: "getOrThrow",
|
|
20378
|
+
id,
|
|
20379
|
+
code: "RESOURCE_NOT_FOUND"
|
|
20380
|
+
});
|
|
20381
|
+
}
|
|
20382
|
+
if (!ok) {
|
|
20383
|
+
throw err;
|
|
20384
|
+
}
|
|
20385
|
+
return data;
|
|
20386
|
+
}
|
|
19989
20387
|
/**
|
|
19990
20388
|
* Check if a resource exists by ID
|
|
19991
20389
|
* @returns {Promise<boolean>} True if resource exists, false otherwise
|
|
19992
20390
|
*/
|
|
19993
20391
|
async exists(id) {
|
|
20392
|
+
await this.executeHooks("beforeExists", { id });
|
|
19994
20393
|
const key = this.getResourceKey(id);
|
|
19995
20394
|
const [ok, err] = await tryFn(() => this.client.headObject(key));
|
|
20395
|
+
await this.executeHooks("afterExists", { id, exists: ok });
|
|
19996
20396
|
return ok;
|
|
19997
20397
|
}
|
|
19998
20398
|
/**
|
|
@@ -20048,7 +20448,7 @@ ${errorDetails}`,
|
|
|
20048
20448
|
}
|
|
20049
20449
|
const preProcessedData = await this.executeHooks("beforeUpdate", cloneDeep(mergedData));
|
|
20050
20450
|
const completeData = { ...originalData, ...preProcessedData, id };
|
|
20051
|
-
const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
|
|
20451
|
+
const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
|
|
20052
20452
|
if (!isValid) {
|
|
20053
20453
|
throw new InvalidResourceItem({
|
|
20054
20454
|
bucket: this.client.config.bucket,
|
|
@@ -20221,12 +20621,17 @@ ${errorDetails}`,
|
|
|
20221
20621
|
if (!fields || typeof fields !== "object") {
|
|
20222
20622
|
throw new Error("fields must be a non-empty object");
|
|
20223
20623
|
}
|
|
20624
|
+
await this.executeHooks("beforePatch", { id, fields, options });
|
|
20224
20625
|
const behavior = this.behavior;
|
|
20225
20626
|
const hasNestedFields = Object.keys(fields).some((key) => key.includes("."));
|
|
20627
|
+
let result;
|
|
20226
20628
|
if ((behavior === "enforce-limits" || behavior === "truncate-data") && !hasNestedFields) {
|
|
20227
|
-
|
|
20629
|
+
result = await this._patchViaCopyObject(id, fields, options);
|
|
20630
|
+
} else {
|
|
20631
|
+
result = await this.update(id, fields, options);
|
|
20228
20632
|
}
|
|
20229
|
-
|
|
20633
|
+
const finalResult = await this.executeHooks("afterPatch", result);
|
|
20634
|
+
return finalResult;
|
|
20230
20635
|
}
|
|
20231
20636
|
/**
|
|
20232
20637
|
* Internal helper: Optimized patch using HeadObject + CopyObject
|
|
@@ -20326,6 +20731,7 @@ ${errorDetails}`,
|
|
|
20326
20731
|
if (!fullData || typeof fullData !== "object") {
|
|
20327
20732
|
throw new Error("fullData must be a non-empty object");
|
|
20328
20733
|
}
|
|
20734
|
+
await this.executeHooks("beforeReplace", { id, fullData, options });
|
|
20329
20735
|
const { partition, partitionValues } = options;
|
|
20330
20736
|
const dataClone = cloneDeep(fullData);
|
|
20331
20737
|
const attributesWithDefaults = this.applyDefaults(dataClone);
|
|
@@ -20340,7 +20746,7 @@ ${errorDetails}`,
|
|
|
20340
20746
|
errors,
|
|
20341
20747
|
isValid,
|
|
20342
20748
|
data: validated
|
|
20343
|
-
} = await this.validate(completeData);
|
|
20749
|
+
} = await this.validate(completeData, { includeId: true });
|
|
20344
20750
|
if (!isValid) {
|
|
20345
20751
|
const errorMsg = errors && errors.length && errors[0].message ? errors[0].message : "Replace failed";
|
|
20346
20752
|
throw new InvalidResourceItem({
|
|
@@ -20413,7 +20819,8 @@ ${errorDetails}`,
|
|
|
20413
20819
|
await this.handlePartitionReferenceUpdates({}, replacedObject);
|
|
20414
20820
|
}
|
|
20415
20821
|
}
|
|
20416
|
-
|
|
20822
|
+
const finalResult = await this.executeHooks("afterReplace", replacedObject);
|
|
20823
|
+
return finalResult;
|
|
20417
20824
|
}
|
|
20418
20825
|
/**
|
|
20419
20826
|
* Update with conditional check (If-Match ETag)
|
|
@@ -20471,7 +20878,7 @@ ${errorDetails}`,
|
|
|
20471
20878
|
}
|
|
20472
20879
|
const preProcessedData = await this.executeHooks("beforeUpdate", cloneDeep(mergedData));
|
|
20473
20880
|
const completeData = { ...originalData, ...preProcessedData, id };
|
|
20474
|
-
const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
|
|
20881
|
+
const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
|
|
20475
20882
|
if (!isValid) {
|
|
20476
20883
|
return {
|
|
20477
20884
|
success: false,
|
|
@@ -20692,6 +21099,7 @@ ${errorDetails}`,
|
|
|
20692
21099
|
* });
|
|
20693
21100
|
*/
|
|
20694
21101
|
async count({ partition = null, partitionValues = {} } = {}) {
|
|
21102
|
+
await this.executeHooks("beforeCount", { partition, partitionValues });
|
|
20695
21103
|
let prefix;
|
|
20696
21104
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
20697
21105
|
const partitionDef = this.config.partitions[partition];
|
|
@@ -20716,6 +21124,7 @@ ${errorDetails}`,
|
|
|
20716
21124
|
prefix = `resource=${this.name}/data`;
|
|
20717
21125
|
}
|
|
20718
21126
|
const count = await this.client.count({ prefix });
|
|
21127
|
+
await this.executeHooks("afterCount", { count, partition, partitionValues });
|
|
20719
21128
|
this.emit("count", count);
|
|
20720
21129
|
return count;
|
|
20721
21130
|
}
|
|
@@ -20751,6 +21160,7 @@ ${errorDetails}`,
|
|
|
20751
21160
|
* const results = await resource.deleteMany(deletedIds);
|
|
20752
21161
|
*/
|
|
20753
21162
|
async deleteMany(ids) {
|
|
21163
|
+
await this.executeHooks("beforeDeleteMany", { ids });
|
|
20754
21164
|
const packages = chunk(
|
|
20755
21165
|
ids.map((id) => this.getResourceKey(id)),
|
|
20756
21166
|
1e3
|
|
@@ -20772,6 +21182,7 @@ ${errorDetails}`,
|
|
|
20772
21182
|
});
|
|
20773
21183
|
return response;
|
|
20774
21184
|
});
|
|
21185
|
+
await this.executeHooks("afterDeleteMany", { ids, results });
|
|
20775
21186
|
this.emit("deleteMany", ids.length);
|
|
20776
21187
|
return results;
|
|
20777
21188
|
}
|
|
@@ -20893,6 +21304,7 @@ ${errorDetails}`,
|
|
|
20893
21304
|
* });
|
|
20894
21305
|
*/
|
|
20895
21306
|
async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
|
|
21307
|
+
await this.executeHooks("beforeList", { partition, partitionValues, limit, offset });
|
|
20896
21308
|
const [ok, err, result] = await tryFn(async () => {
|
|
20897
21309
|
if (!partition) {
|
|
20898
21310
|
return await this.listMain({ limit, offset });
|
|
@@ -20902,7 +21314,8 @@ ${errorDetails}`,
|
|
|
20902
21314
|
if (!ok) {
|
|
20903
21315
|
return this.handleListError(err, { partition, partitionValues });
|
|
20904
21316
|
}
|
|
20905
|
-
|
|
21317
|
+
const finalResult = await this.executeHooks("afterList", result);
|
|
21318
|
+
return finalResult;
|
|
20906
21319
|
}
|
|
20907
21320
|
async listMain({ limit, offset = 0 }) {
|
|
20908
21321
|
const [ok, err, ids] = await tryFn(() => this.listIds({ limit, offset }));
|
|
@@ -21045,6 +21458,7 @@ ${errorDetails}`,
|
|
|
21045
21458
|
* const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
|
|
21046
21459
|
*/
|
|
21047
21460
|
async getMany(ids) {
|
|
21461
|
+
await this.executeHooks("beforeGetMany", { ids });
|
|
21048
21462
|
const { results, errors } = await PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
|
|
21049
21463
|
this.emit("error", error, content);
|
|
21050
21464
|
this.observers.map((x) => x.emit("error", this.name, error, content));
|
|
@@ -21065,8 +21479,9 @@ ${errorDetails}`,
|
|
|
21065
21479
|
}
|
|
21066
21480
|
throw err;
|
|
21067
21481
|
});
|
|
21482
|
+
const finalResults = await this.executeHooks("afterGetMany", results);
|
|
21068
21483
|
this.emit("getMany", ids.length);
|
|
21069
|
-
return
|
|
21484
|
+
return finalResults;
|
|
21070
21485
|
}
|
|
21071
21486
|
/**
|
|
21072
21487
|
* Get all resources (equivalent to list() without pagination)
|
|
@@ -21313,21 +21728,6 @@ ${errorDetails}`,
|
|
|
21313
21728
|
* @returns {Object} Schema object for the version
|
|
21314
21729
|
*/
|
|
21315
21730
|
async getSchemaForVersion(version) {
|
|
21316
|
-
if (version === this.version) {
|
|
21317
|
-
return this.schema;
|
|
21318
|
-
}
|
|
21319
|
-
const [ok, err, compatibleSchema] = await tryFn(() => Promise.resolve(new Schema({
|
|
21320
|
-
name: this.name,
|
|
21321
|
-
attributes: this.attributes,
|
|
21322
|
-
passphrase: this.passphrase,
|
|
21323
|
-
version,
|
|
21324
|
-
options: {
|
|
21325
|
-
...this.config,
|
|
21326
|
-
autoDecrypt: true,
|
|
21327
|
-
autoEncrypt: true
|
|
21328
|
-
}
|
|
21329
|
-
})));
|
|
21330
|
-
if (ok) return compatibleSchema;
|
|
21331
21731
|
return this.schema;
|
|
21332
21732
|
}
|
|
21333
21733
|
/**
|
|
@@ -21423,6 +21823,7 @@ ${errorDetails}`,
|
|
|
21423
21823
|
* );
|
|
21424
21824
|
*/
|
|
21425
21825
|
async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
|
|
21826
|
+
await this.executeHooks("beforeQuery", { filter, limit, offset, partition, partitionValues });
|
|
21426
21827
|
if (Object.keys(filter).length === 0) {
|
|
21427
21828
|
return await this.list({ partition, partitionValues, limit, offset });
|
|
21428
21829
|
}
|
|
@@ -21450,7 +21851,8 @@ ${errorDetails}`,
|
|
|
21450
21851
|
break;
|
|
21451
21852
|
}
|
|
21452
21853
|
}
|
|
21453
|
-
|
|
21854
|
+
const finalResults = results.slice(0, limit);
|
|
21855
|
+
return await this.executeHooks("afterQuery", finalResults);
|
|
21454
21856
|
}
|
|
21455
21857
|
/**
|
|
21456
21858
|
* Handle partition reference updates with change detection
|
|
@@ -21530,7 +21932,7 @@ ${errorDetails}`,
|
|
|
21530
21932
|
}
|
|
21531
21933
|
}
|
|
21532
21934
|
/**
|
|
21533
|
-
* Update partition objects to keep them in sync
|
|
21935
|
+
* Update partition objects to keep them in sync
|
|
21534
21936
|
* @param {Object} data - Updated object data
|
|
21535
21937
|
*/
|
|
21536
21938
|
async updatePartitionReferences(data) {
|
|
@@ -21916,7 +22318,32 @@ function validateResourceConfig(config) {
|
|
|
21916
22318
|
if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
|
|
21917
22319
|
errors.push("Resource 'hooks' must be an object");
|
|
21918
22320
|
} else {
|
|
21919
|
-
const validHookEvents = [
|
|
22321
|
+
const validHookEvents = [
|
|
22322
|
+
"beforeInsert",
|
|
22323
|
+
"afterInsert",
|
|
22324
|
+
"beforeUpdate",
|
|
22325
|
+
"afterUpdate",
|
|
22326
|
+
"beforeDelete",
|
|
22327
|
+
"afterDelete",
|
|
22328
|
+
"beforeGet",
|
|
22329
|
+
"afterGet",
|
|
22330
|
+
"beforeList",
|
|
22331
|
+
"afterList",
|
|
22332
|
+
"beforeQuery",
|
|
22333
|
+
"afterQuery",
|
|
22334
|
+
"beforeExists",
|
|
22335
|
+
"afterExists",
|
|
22336
|
+
"beforeCount",
|
|
22337
|
+
"afterCount",
|
|
22338
|
+
"beforePatch",
|
|
22339
|
+
"afterPatch",
|
|
22340
|
+
"beforeReplace",
|
|
22341
|
+
"afterReplace",
|
|
22342
|
+
"beforeGetMany",
|
|
22343
|
+
"afterGetMany",
|
|
22344
|
+
"beforeDeleteMany",
|
|
22345
|
+
"afterDeleteMany"
|
|
22346
|
+
];
|
|
21920
22347
|
for (const [event, hooksArr] of Object.entries(config.hooks)) {
|
|
21921
22348
|
if (!validHookEvents.includes(event)) {
|
|
21922
22349
|
errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
|
|
@@ -21964,17 +22391,35 @@ class Database extends EventEmitter {
|
|
|
21964
22391
|
this.id = idGenerator(7);
|
|
21965
22392
|
this.version = "1";
|
|
21966
22393
|
this.s3dbVersion = (() => {
|
|
21967
|
-
const [ok, err, version] = tryFn(() => true ? "12.
|
|
22394
|
+
const [ok, err, version] = tryFn(() => true ? "12.2.0" : "latest");
|
|
21968
22395
|
return ok ? version : "latest";
|
|
21969
22396
|
})();
|
|
21970
|
-
this.
|
|
22397
|
+
this._resourcesMap = {};
|
|
22398
|
+
this.resources = new Proxy(this._resourcesMap, {
|
|
22399
|
+
get: (target, prop) => {
|
|
22400
|
+
if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
|
|
22401
|
+
return target[prop];
|
|
22402
|
+
}
|
|
22403
|
+
if (target[prop]) {
|
|
22404
|
+
return target[prop];
|
|
22405
|
+
}
|
|
22406
|
+
return void 0;
|
|
22407
|
+
},
|
|
22408
|
+
// Support Object.keys(), Object.entries(), etc.
|
|
22409
|
+
ownKeys: (target) => {
|
|
22410
|
+
return Object.keys(target);
|
|
22411
|
+
},
|
|
22412
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
22413
|
+
return Object.getOwnPropertyDescriptor(target, prop);
|
|
22414
|
+
}
|
|
22415
|
+
});
|
|
21971
22416
|
this.savedMetadata = null;
|
|
21972
22417
|
this.options = options;
|
|
21973
22418
|
this.verbose = options.verbose || false;
|
|
21974
22419
|
this.parallelism = parseInt(options.parallelism + "") || 10;
|
|
21975
|
-
this.plugins = options.plugins || [];
|
|
21976
|
-
this.pluginRegistry = {};
|
|
21977
22420
|
this.pluginList = options.plugins || [];
|
|
22421
|
+
this.pluginRegistry = {};
|
|
22422
|
+
this.plugins = this.pluginRegistry;
|
|
21978
22423
|
this.cache = options.cache;
|
|
21979
22424
|
this.passphrase = options.passphrase || "secret";
|
|
21980
22425
|
this.versioningEnabled = options.versioningEnabled || false;
|
|
@@ -22080,7 +22525,7 @@ class Database extends EventEmitter {
|
|
|
22080
22525
|
} else {
|
|
22081
22526
|
restoredIdSize = versionData.idSize || 22;
|
|
22082
22527
|
}
|
|
22083
|
-
this.
|
|
22528
|
+
this._resourcesMap[name] = new Resource({
|
|
22084
22529
|
name,
|
|
22085
22530
|
client: this.client,
|
|
22086
22531
|
database: this,
|
|
@@ -22149,7 +22594,7 @@ class Database extends EventEmitter {
|
|
|
22149
22594
|
}
|
|
22150
22595
|
}
|
|
22151
22596
|
for (const [name, savedResource] of Object.entries(savedMetadata.resources || {})) {
|
|
22152
|
-
if (!this.
|
|
22597
|
+
if (!this._resourcesMap[name]) {
|
|
22153
22598
|
const currentVersion = savedResource.currentVersion || "v1";
|
|
22154
22599
|
const versionData = savedResource.versions?.[currentVersion];
|
|
22155
22600
|
changes.push({
|
|
@@ -22661,7 +23106,7 @@ class Database extends EventEmitter {
|
|
|
22661
23106
|
* @returns {boolean} True if resource exists, false otherwise
|
|
22662
23107
|
*/
|
|
22663
23108
|
resourceExists(name) {
|
|
22664
|
-
return !!this.
|
|
23109
|
+
return !!this._resourcesMap[name];
|
|
22665
23110
|
}
|
|
22666
23111
|
/**
|
|
22667
23112
|
* Check if a resource exists with the same definition hash
|
|
@@ -22669,14 +23114,13 @@ class Database extends EventEmitter {
|
|
|
22669
23114
|
* @param {string} config.name - Resource name
|
|
22670
23115
|
* @param {Object} config.attributes - Resource attributes
|
|
22671
23116
|
* @param {string} [config.behavior] - Resource behavior
|
|
22672
|
-
* @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
|
|
22673
23117
|
* @returns {Object} Result with exists and hash information
|
|
22674
23118
|
*/
|
|
22675
|
-
resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", partitions = {}
|
|
22676
|
-
if (!this.
|
|
23119
|
+
resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", partitions = {} }) {
|
|
23120
|
+
if (!this._resourcesMap[name]) {
|
|
22677
23121
|
return { exists: false, sameHash: false, hash: null };
|
|
22678
23122
|
}
|
|
22679
|
-
const existingResource = this.
|
|
23123
|
+
const existingResource = this._resourcesMap[name];
|
|
22680
23124
|
const existingHash = this.generateDefinitionHash(existingResource.export());
|
|
22681
23125
|
const mockResource = new Resource({
|
|
22682
23126
|
name,
|
|
@@ -22686,8 +23130,7 @@ class Database extends EventEmitter {
|
|
|
22686
23130
|
client: this.client,
|
|
22687
23131
|
version: existingResource.version,
|
|
22688
23132
|
passphrase: this.passphrase,
|
|
22689
|
-
versioningEnabled: this.versioningEnabled
|
|
22690
|
-
...options
|
|
23133
|
+
versioningEnabled: this.versioningEnabled
|
|
22691
23134
|
});
|
|
22692
23135
|
const newHash = this.generateDefinitionHash(mockResource.export());
|
|
22693
23136
|
return {
|
|
@@ -22715,12 +23158,49 @@ class Database extends EventEmitter {
|
|
|
22715
23158
|
* @param {string} [config.createdBy='user'] - Who created this resource ('user', 'plugin', or plugin name)
|
|
22716
23159
|
* @returns {Promise<Resource>} The created or updated resource
|
|
22717
23160
|
*/
|
|
22718
|
-
|
|
22719
|
-
|
|
22720
|
-
|
|
23161
|
+
/**
|
|
23162
|
+
* Normalize partitions config from array or object format
|
|
23163
|
+
* @param {Array|Object} partitions - Partitions config
|
|
23164
|
+
* @param {Object} attributes - Resource attributes
|
|
23165
|
+
* @returns {Object} Normalized partitions object
|
|
23166
|
+
* @private
|
|
23167
|
+
*/
|
|
23168
|
+
_normalizePartitions(partitions, attributes) {
|
|
23169
|
+
if (!Array.isArray(partitions)) {
|
|
23170
|
+
return partitions || {};
|
|
23171
|
+
}
|
|
23172
|
+
const normalized = {};
|
|
23173
|
+
for (const fieldName of partitions) {
|
|
23174
|
+
if (typeof fieldName !== "string") {
|
|
23175
|
+
throw new Error(`Partition field must be a string, got ${typeof fieldName}`);
|
|
23176
|
+
}
|
|
23177
|
+
if (!attributes[fieldName]) {
|
|
23178
|
+
throw new Error(`Partition field '${fieldName}' not found in attributes`);
|
|
23179
|
+
}
|
|
23180
|
+
const partitionName = `by${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
|
|
23181
|
+
const fieldDef = attributes[fieldName];
|
|
23182
|
+
let fieldType = "string";
|
|
23183
|
+
if (typeof fieldDef === "string") {
|
|
23184
|
+
fieldType = fieldDef.split("|")[0].trim();
|
|
23185
|
+
} else if (typeof fieldDef === "object" && fieldDef.type) {
|
|
23186
|
+
fieldType = fieldDef.type;
|
|
23187
|
+
}
|
|
23188
|
+
normalized[partitionName] = {
|
|
23189
|
+
fields: {
|
|
23190
|
+
[fieldName]: fieldType
|
|
23191
|
+
}
|
|
23192
|
+
};
|
|
23193
|
+
}
|
|
23194
|
+
return normalized;
|
|
23195
|
+
}
|
|
23196
|
+
async createResource({ name, attributes, behavior = "user-managed", hooks, middlewares, ...config }) {
|
|
23197
|
+
const normalizedPartitions = this._normalizePartitions(config.partitions, attributes);
|
|
23198
|
+
if (this._resourcesMap[name]) {
|
|
23199
|
+
const existingResource = this._resourcesMap[name];
|
|
22721
23200
|
Object.assign(existingResource.config, {
|
|
22722
23201
|
cache: this.cache,
|
|
22723
|
-
...config
|
|
23202
|
+
...config,
|
|
23203
|
+
partitions: normalizedPartitions
|
|
22724
23204
|
});
|
|
22725
23205
|
if (behavior) {
|
|
22726
23206
|
existingResource.behavior = behavior;
|
|
@@ -22738,6 +23218,9 @@ class Database extends EventEmitter {
|
|
|
22738
23218
|
}
|
|
22739
23219
|
}
|
|
22740
23220
|
}
|
|
23221
|
+
if (middlewares) {
|
|
23222
|
+
this._applyMiddlewares(existingResource, middlewares);
|
|
23223
|
+
}
|
|
22741
23224
|
const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
|
|
22742
23225
|
const existingMetadata2 = this.savedMetadata?.resources?.[name];
|
|
22743
23226
|
const currentVersion = existingMetadata2?.currentVersion || "v1";
|
|
@@ -22761,7 +23244,7 @@ class Database extends EventEmitter {
|
|
|
22761
23244
|
observers: [this],
|
|
22762
23245
|
cache: config.cache !== void 0 ? config.cache : this.cache,
|
|
22763
23246
|
timestamps: config.timestamps !== void 0 ? config.timestamps : false,
|
|
22764
|
-
partitions:
|
|
23247
|
+
partitions: normalizedPartitions,
|
|
22765
23248
|
paranoid: config.paranoid !== void 0 ? config.paranoid : true,
|
|
22766
23249
|
allNestedObjectsOptional: config.allNestedObjectsOptional !== void 0 ? config.allNestedObjectsOptional : true,
|
|
22767
23250
|
autoDecrypt: config.autoDecrypt !== void 0 ? config.autoDecrypt : true,
|
|
@@ -22777,16 +23260,96 @@ class Database extends EventEmitter {
|
|
|
22777
23260
|
createdBy: config.createdBy || "user"
|
|
22778
23261
|
});
|
|
22779
23262
|
resource.database = this;
|
|
22780
|
-
this.
|
|
23263
|
+
this._resourcesMap[name] = resource;
|
|
23264
|
+
if (middlewares) {
|
|
23265
|
+
this._applyMiddlewares(resource, middlewares);
|
|
23266
|
+
}
|
|
22781
23267
|
await this.uploadMetadataFile();
|
|
22782
23268
|
this.emit("s3db.resourceCreated", name);
|
|
22783
23269
|
return resource;
|
|
22784
23270
|
}
|
|
22785
|
-
|
|
22786
|
-
|
|
22787
|
-
|
|
23271
|
+
/**
|
|
23272
|
+
* Apply middlewares to a resource
|
|
23273
|
+
* @param {Resource} resource - Resource instance
|
|
23274
|
+
* @param {Array|Object} middlewares - Middlewares config
|
|
23275
|
+
* @private
|
|
23276
|
+
*/
|
|
23277
|
+
_applyMiddlewares(resource, middlewares) {
|
|
23278
|
+
if (Array.isArray(middlewares)) {
|
|
23279
|
+
const methods = resource._middlewareMethods || [
|
|
23280
|
+
"get",
|
|
23281
|
+
"list",
|
|
23282
|
+
"listIds",
|
|
23283
|
+
"getAll",
|
|
23284
|
+
"count",
|
|
23285
|
+
"page",
|
|
23286
|
+
"insert",
|
|
23287
|
+
"update",
|
|
23288
|
+
"delete",
|
|
23289
|
+
"deleteMany",
|
|
23290
|
+
"exists",
|
|
23291
|
+
"getMany",
|
|
23292
|
+
"content",
|
|
23293
|
+
"hasContent",
|
|
23294
|
+
"query",
|
|
23295
|
+
"getFromPartition",
|
|
23296
|
+
"setContent",
|
|
23297
|
+
"deleteContent",
|
|
23298
|
+
"replace",
|
|
23299
|
+
"patch"
|
|
23300
|
+
];
|
|
23301
|
+
for (const method of methods) {
|
|
23302
|
+
for (const middleware of middlewares) {
|
|
23303
|
+
if (typeof middleware === "function") {
|
|
23304
|
+
resource.useMiddleware(method, middleware);
|
|
23305
|
+
}
|
|
23306
|
+
}
|
|
23307
|
+
}
|
|
23308
|
+
return;
|
|
23309
|
+
}
|
|
23310
|
+
if (typeof middlewares === "object" && middlewares !== null) {
|
|
23311
|
+
for (const [method, fns] of Object.entries(middlewares)) {
|
|
23312
|
+
if (method === "*") {
|
|
23313
|
+
const methods = resource._middlewareMethods || [
|
|
23314
|
+
"get",
|
|
23315
|
+
"list",
|
|
23316
|
+
"listIds",
|
|
23317
|
+
"getAll",
|
|
23318
|
+
"count",
|
|
23319
|
+
"page",
|
|
23320
|
+
"insert",
|
|
23321
|
+
"update",
|
|
23322
|
+
"delete",
|
|
23323
|
+
"deleteMany",
|
|
23324
|
+
"exists",
|
|
23325
|
+
"getMany",
|
|
23326
|
+
"content",
|
|
23327
|
+
"hasContent",
|
|
23328
|
+
"query",
|
|
23329
|
+
"getFromPartition",
|
|
23330
|
+
"setContent",
|
|
23331
|
+
"deleteContent",
|
|
23332
|
+
"replace",
|
|
23333
|
+
"patch"
|
|
23334
|
+
];
|
|
23335
|
+
const middlewareArray = Array.isArray(fns) ? fns : [fns];
|
|
23336
|
+
for (const targetMethod of methods) {
|
|
23337
|
+
for (const middleware of middlewareArray) {
|
|
23338
|
+
if (typeof middleware === "function") {
|
|
23339
|
+
resource.useMiddleware(targetMethod, middleware);
|
|
23340
|
+
}
|
|
23341
|
+
}
|
|
23342
|
+
}
|
|
23343
|
+
} else {
|
|
23344
|
+
const middlewareArray = Array.isArray(fns) ? fns : [fns];
|
|
23345
|
+
for (const middleware of middlewareArray) {
|
|
23346
|
+
if (typeof middleware === "function") {
|
|
23347
|
+
resource.useMiddleware(method, middleware);
|
|
23348
|
+
}
|
|
23349
|
+
}
|
|
23350
|
+
}
|
|
23351
|
+
}
|
|
22788
23352
|
}
|
|
22789
|
-
return this.resources[name];
|
|
22790
23353
|
}
|
|
22791
23354
|
/**
|
|
22792
23355
|
* List all resource names
|
|
@@ -22801,14 +23364,14 @@ class Database extends EventEmitter {
|
|
|
22801
23364
|
* @returns {Resource} Resource instance
|
|
22802
23365
|
*/
|
|
22803
23366
|
async getResource(name) {
|
|
22804
|
-
if (!this.
|
|
23367
|
+
if (!this._resourcesMap[name]) {
|
|
22805
23368
|
throw new ResourceNotFound({
|
|
22806
23369
|
bucket: this.client.config.bucket,
|
|
22807
23370
|
resourceName: name,
|
|
22808
23371
|
id: name
|
|
22809
23372
|
});
|
|
22810
23373
|
}
|
|
22811
|
-
return this.
|
|
23374
|
+
return this._resourcesMap[name];
|
|
22812
23375
|
}
|
|
22813
23376
|
/**
|
|
22814
23377
|
* Get database configuration
|
|
@@ -22861,7 +23424,7 @@ class Database extends EventEmitter {
|
|
|
22861
23424
|
}
|
|
22862
23425
|
});
|
|
22863
23426
|
}
|
|
22864
|
-
Object.keys(this.resources).forEach((k) => delete this.
|
|
23427
|
+
Object.keys(this.resources).forEach((k) => delete this._resourcesMap[k]);
|
|
22865
23428
|
}
|
|
22866
23429
|
if (this.client && typeof this.client.removeAllListeners === "function") {
|
|
22867
23430
|
this.client.removeAllListeners();
|
|
@@ -24605,14 +25168,6 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24605
25168
|
}
|
|
24606
25169
|
async start() {
|
|
24607
25170
|
}
|
|
24608
|
-
async stop() {
|
|
24609
|
-
for (const replicator of this.replicators || []) {
|
|
24610
|
-
if (replicator && typeof replicator.cleanup === "function") {
|
|
24611
|
-
await replicator.cleanup();
|
|
24612
|
-
}
|
|
24613
|
-
}
|
|
24614
|
-
this.removeDatabaseHooks();
|
|
24615
|
-
}
|
|
24616
25171
|
installDatabaseHooks() {
|
|
24617
25172
|
this._afterCreateResourceHook = (resource) => {
|
|
24618
25173
|
if (resource.name !== (this.config.replicatorLogResource || "plg_replicator_logs")) {
|
|
@@ -24942,20 +25497,20 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24942
25497
|
}
|
|
24943
25498
|
this.emit("replicator.sync.completed", { replicatorId, stats: this.stats });
|
|
24944
25499
|
}
|
|
24945
|
-
async
|
|
25500
|
+
async stop() {
|
|
24946
25501
|
const [ok, error] = await tryFn(async () => {
|
|
24947
25502
|
if (this.replicators && this.replicators.length > 0) {
|
|
24948
25503
|
const cleanupPromises = this.replicators.map(async (replicator) => {
|
|
24949
25504
|
const [replicatorOk, replicatorError] = await tryFn(async () => {
|
|
24950
|
-
if (replicator && typeof replicator.
|
|
24951
|
-
await replicator.
|
|
25505
|
+
if (replicator && typeof replicator.stop === "function") {
|
|
25506
|
+
await replicator.stop();
|
|
24952
25507
|
}
|
|
24953
25508
|
});
|
|
24954
25509
|
if (!replicatorOk) {
|
|
24955
25510
|
if (this.config.verbose) {
|
|
24956
|
-
console.warn(`[ReplicatorPlugin] Failed to
|
|
25511
|
+
console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
|
|
24957
25512
|
}
|
|
24958
|
-
this.emit("
|
|
25513
|
+
this.emit("replicator_stop_error", {
|
|
24959
25514
|
replicator: replicator.name || replicator.id || "unknown",
|
|
24960
25515
|
driver: replicator.driver || "unknown",
|
|
24961
25516
|
error: replicatorError.message
|
|
@@ -24964,6 +25519,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24964
25519
|
});
|
|
24965
25520
|
await Promise.allSettled(cleanupPromises);
|
|
24966
25521
|
}
|
|
25522
|
+
this.removeDatabaseHooks();
|
|
24967
25523
|
if (this.database && this.database.resources) {
|
|
24968
25524
|
for (const resourceName of this.eventListenersInstalled) {
|
|
24969
25525
|
const resource = this.database.resources[resourceName];
|
|
@@ -24983,9 +25539,9 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24983
25539
|
});
|
|
24984
25540
|
if (!ok) {
|
|
24985
25541
|
if (this.config.verbose) {
|
|
24986
|
-
console.warn(`[ReplicatorPlugin] Failed to
|
|
25542
|
+
console.warn(`[ReplicatorPlugin] Failed to stop plugin: ${error.message}`);
|
|
24987
25543
|
}
|
|
24988
|
-
this.emit("
|
|
25544
|
+
this.emit("replicator_plugin_stop_error", {
|
|
24989
25545
|
error: error.message
|
|
24990
25546
|
});
|
|
24991
25547
|
}
|
|
@@ -25805,7 +26361,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
25805
26361
|
}
|
|
25806
26362
|
async _persistJobExecution(jobName, executionId, startTime, endTime, duration, status, result, error, retryCount) {
|
|
25807
26363
|
const [ok, err] = await tryFn(
|
|
25808
|
-
() => this.database.
|
|
26364
|
+
() => this.database.resources[this.config.jobHistoryResource].insert({
|
|
25809
26365
|
id: executionId,
|
|
25810
26366
|
jobName,
|
|
25811
26367
|
status,
|
|
@@ -25946,7 +26502,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
25946
26502
|
queryParams.status = status;
|
|
25947
26503
|
}
|
|
25948
26504
|
const [ok, err, history] = await tryFn(
|
|
25949
|
-
() => this.database.
|
|
26505
|
+
() => this.database.resources[this.config.jobHistoryResource].query(queryParams)
|
|
25950
26506
|
);
|
|
25951
26507
|
if (!ok) {
|
|
25952
26508
|
if (this.config.verbose) {
|
|
@@ -26085,9 +26641,6 @@ class SchedulerPlugin extends Plugin {
|
|
|
26085
26641
|
if (this._isTestEnvironment()) {
|
|
26086
26642
|
this.activeJobs.clear();
|
|
26087
26643
|
}
|
|
26088
|
-
}
|
|
26089
|
-
async cleanup() {
|
|
26090
|
-
await this.stop();
|
|
26091
26644
|
this.jobs.clear();
|
|
26092
26645
|
this.statistics.clear();
|
|
26093
26646
|
this.activeJobs.clear();
|
|
@@ -26336,7 +26889,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26336
26889
|
let lastLogErr;
|
|
26337
26890
|
for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
|
|
26338
26891
|
const [ok, err] = await tryFn(
|
|
26339
|
-
() => this.database.
|
|
26892
|
+
() => this.database.resources[this.config.transitionLogResource].insert({
|
|
26340
26893
|
id: transitionId,
|
|
26341
26894
|
machineId,
|
|
26342
26895
|
entityId,
|
|
@@ -26372,11 +26925,11 @@ class StateMachinePlugin extends Plugin {
|
|
|
26372
26925
|
updatedAt: now
|
|
26373
26926
|
};
|
|
26374
26927
|
const [updateOk] = await tryFn(
|
|
26375
|
-
() => this.database.
|
|
26928
|
+
() => this.database.resources[this.config.stateResource].update(stateId, stateData)
|
|
26376
26929
|
);
|
|
26377
26930
|
if (!updateOk) {
|
|
26378
26931
|
const [insertOk, insertErr] = await tryFn(
|
|
26379
|
-
() => this.database.
|
|
26932
|
+
() => this.database.resources[this.config.stateResource].insert({ id: stateId, ...stateData })
|
|
26380
26933
|
);
|
|
26381
26934
|
if (!insertOk && this.config.verbose) {
|
|
26382
26935
|
console.warn(`[StateMachinePlugin] Failed to upsert state:`, insertErr.message);
|
|
@@ -26439,7 +26992,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26439
26992
|
if (this.config.persistTransitions) {
|
|
26440
26993
|
const stateId = `${machineId}_${entityId}`;
|
|
26441
26994
|
const [ok, err, stateRecord] = await tryFn(
|
|
26442
|
-
() => this.database.
|
|
26995
|
+
() => this.database.resources[this.config.stateResource].get(stateId)
|
|
26443
26996
|
);
|
|
26444
26997
|
if (ok && stateRecord) {
|
|
26445
26998
|
machine.currentStates.set(entityId, stateRecord.currentState);
|
|
@@ -26482,7 +27035,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26482
27035
|
}
|
|
26483
27036
|
const { limit = 50, offset = 0 } = options;
|
|
26484
27037
|
const [ok, err, transitions] = await tryFn(
|
|
26485
|
-
() => this.database.
|
|
27038
|
+
() => this.database.resources[this.config.transitionLogResource].query({
|
|
26486
27039
|
machineId,
|
|
26487
27040
|
entityId
|
|
26488
27041
|
}, {
|
|
@@ -26524,7 +27077,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26524
27077
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26525
27078
|
const stateId = `${machineId}_${entityId}`;
|
|
26526
27079
|
const [ok, err] = await tryFn(
|
|
26527
|
-
() => this.database.
|
|
27080
|
+
() => this.database.resources[this.config.stateResource].insert({
|
|
26528
27081
|
id: stateId,
|
|
26529
27082
|
machineId,
|
|
26530
27083
|
entityId,
|
|
@@ -26613,9 +27166,6 @@ class StateMachinePlugin extends Plugin {
|
|
|
26613
27166
|
}
|
|
26614
27167
|
async stop() {
|
|
26615
27168
|
this.machines.clear();
|
|
26616
|
-
}
|
|
26617
|
-
async cleanup() {
|
|
26618
|
-
await this.stop();
|
|
26619
27169
|
this.removeAllListeners();
|
|
26620
27170
|
}
|
|
26621
27171
|
}
|
|
@@ -26630,7 +27180,7 @@ class TfStateError extends Error {
|
|
|
26630
27180
|
}
|
|
26631
27181
|
class InvalidStateFileError extends TfStateError {
|
|
26632
27182
|
constructor(filePath, reason, context = {}) {
|
|
26633
|
-
super(`Invalid
|
|
27183
|
+
super(`Invalid Tfstate file "${filePath}": ${reason}`, context);
|
|
26634
27184
|
this.name = "InvalidStateFileError";
|
|
26635
27185
|
this.filePath = filePath;
|
|
26636
27186
|
this.reason = reason;
|
|
@@ -26639,7 +27189,7 @@ class InvalidStateFileError extends TfStateError {
|
|
|
26639
27189
|
class UnsupportedStateVersionError extends TfStateError {
|
|
26640
27190
|
constructor(version, supportedVersions, context = {}) {
|
|
26641
27191
|
super(
|
|
26642
|
-
`
|
|
27192
|
+
`Tfstate version ${version} is not supported. Supported versions: ${supportedVersions.join(", ")}`,
|
|
26643
27193
|
context
|
|
26644
27194
|
);
|
|
26645
27195
|
this.name = "UnsupportedStateVersionError";
|
|
@@ -26649,7 +27199,7 @@ class UnsupportedStateVersionError extends TfStateError {
|
|
|
26649
27199
|
}
|
|
26650
27200
|
class StateFileNotFoundError extends TfStateError {
|
|
26651
27201
|
constructor(filePath, context = {}) {
|
|
26652
|
-
super(`
|
|
27202
|
+
super(`Tfstate file not found: ${filePath}`, context);
|
|
26653
27203
|
this.name = "StateFileNotFoundError";
|
|
26654
27204
|
this.filePath = filePath;
|
|
26655
27205
|
}
|
|
@@ -34872,42 +35422,23 @@ class FilesystemTfStateDriver extends TfStateDriver {
|
|
|
34872
35422
|
class TfStatePlugin extends Plugin {
|
|
34873
35423
|
constructor(config = {}) {
|
|
34874
35424
|
super(config);
|
|
34875
|
-
|
|
34876
|
-
|
|
34877
|
-
|
|
34878
|
-
|
|
34879
|
-
|
|
34880
|
-
|
|
34881
|
-
|
|
34882
|
-
|
|
34883
|
-
|
|
34884
|
-
|
|
34885
|
-
|
|
34886
|
-
|
|
34887
|
-
|
|
34888
|
-
|
|
34889
|
-
|
|
34890
|
-
|
|
34891
|
-
|
|
34892
|
-
this.filters = config.filters || {};
|
|
34893
|
-
this.verbose = config.verbose || false;
|
|
34894
|
-
} else {
|
|
34895
|
-
this.driverType = null;
|
|
34896
|
-
this.driverConfig = {};
|
|
34897
|
-
this.resourceName = config.resourceName || "plg_tfstate_resources";
|
|
34898
|
-
this.stateFilesName = config.stateFilesName || "plg_tfstate_state_files";
|
|
34899
|
-
this.diffsName = config.diffsName || config.stateHistoryName || "plg_tfstate_state_diffs";
|
|
34900
|
-
this.stateHistoryName = this.diffsName;
|
|
34901
|
-
this.autoSync = config.autoSync || false;
|
|
34902
|
-
this.watchPaths = Array.isArray(config.watchPaths) ? config.watchPaths : [];
|
|
34903
|
-
this.filters = config.filters || {};
|
|
34904
|
-
this.trackDiffs = config.trackDiffs !== void 0 ? config.trackDiffs : true;
|
|
34905
|
-
this.diffsLookback = 10;
|
|
34906
|
-
this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
|
|
34907
|
-
this.verbose = config.verbose || false;
|
|
34908
|
-
this.monitorEnabled = false;
|
|
34909
|
-
this.monitorCron = "*/5 * * * *";
|
|
34910
|
-
}
|
|
35425
|
+
this.driverType = config.driver || null;
|
|
35426
|
+
this.driverConfig = config.config || {};
|
|
35427
|
+
const resources = config.resources || {};
|
|
35428
|
+
this.resourceName = resources.resources || config.resourceName || "plg_tfstate_resources";
|
|
35429
|
+
this.stateFilesName = resources.stateFiles || config.stateFilesName || "plg_tfstate_state_files";
|
|
35430
|
+
this.diffsName = resources.diffs || config.diffsName || "plg_tfstate_state_diffs";
|
|
35431
|
+
const monitor = config.monitor || {};
|
|
35432
|
+
this.monitorEnabled = monitor.enabled || false;
|
|
35433
|
+
this.monitorCron = monitor.cron || "*/5 * * * *";
|
|
35434
|
+
const diffs = config.diffs || {};
|
|
35435
|
+
this.trackDiffs = diffs.enabled !== void 0 ? diffs.enabled : config.trackDiffs !== void 0 ? config.trackDiffs : true;
|
|
35436
|
+
this.diffsLookback = diffs.lookback || 10;
|
|
35437
|
+
this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
|
|
35438
|
+
this.autoSync = config.autoSync || false;
|
|
35439
|
+
this.watchPaths = config.watchPaths || [];
|
|
35440
|
+
this.filters = config.filters || {};
|
|
35441
|
+
this.verbose = config.verbose || false;
|
|
34911
35442
|
this.supportedVersions = [3, 4];
|
|
34912
35443
|
this.driver = null;
|
|
34913
35444
|
this.resource = null;
|
|
@@ -34957,7 +35488,7 @@ class TfStatePlugin extends Plugin {
|
|
|
34957
35488
|
name: this.lineagesName,
|
|
34958
35489
|
attributes: {
|
|
34959
35490
|
id: "string|required",
|
|
34960
|
-
// = lineage UUID from
|
|
35491
|
+
// = lineage UUID from Tfstate
|
|
34961
35492
|
latestSerial: "number",
|
|
34962
35493
|
// Track latest for quick access
|
|
34963
35494
|
latestStateId: "string",
|
|
@@ -35670,7 +36201,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35670
36201
|
return result;
|
|
35671
36202
|
}
|
|
35672
36203
|
/**
|
|
35673
|
-
* Read and parse
|
|
36204
|
+
* Read and parse Tfstate file
|
|
35674
36205
|
* @private
|
|
35675
36206
|
*/
|
|
35676
36207
|
async _readStateFile(filePath) {
|
|
@@ -35707,7 +36238,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35707
36238
|
}
|
|
35708
36239
|
}
|
|
35709
36240
|
/**
|
|
35710
|
-
* Validate
|
|
36241
|
+
* Validate Tfstate version
|
|
35711
36242
|
* @private
|
|
35712
36243
|
*/
|
|
35713
36244
|
_validateStateVersion(state) {
|
|
@@ -35720,7 +36251,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35720
36251
|
}
|
|
35721
36252
|
}
|
|
35722
36253
|
/**
|
|
35723
|
-
* Extract resources from
|
|
36254
|
+
* Extract resources from Tfstate
|
|
35724
36255
|
* @private
|
|
35725
36256
|
*/
|
|
35726
36257
|
async _extractResources(state, filePath, stateFileId, lineageId) {
|
|
@@ -36287,14 +36818,14 @@ class TfStatePlugin extends Plugin {
|
|
|
36287
36818
|
}
|
|
36288
36819
|
}
|
|
36289
36820
|
/**
|
|
36290
|
-
* Export resources to
|
|
36821
|
+
* Export resources to Tfstate format
|
|
36291
36822
|
* @param {Object} options - Export options
|
|
36292
36823
|
* @param {number} options.serial - Specific serial to export (default: latest)
|
|
36293
36824
|
* @param {string[]} options.resourceTypes - Filter by resource types
|
|
36294
36825
|
* @param {string} options.terraformVersion - Terraform version for output (default: '1.5.0')
|
|
36295
36826
|
* @param {string} options.lineage - State lineage (default: auto-generated)
|
|
36296
36827
|
* @param {Object} options.outputs - Terraform outputs to include
|
|
36297
|
-
* @returns {Promise<Object>}
|
|
36828
|
+
* @returns {Promise<Object>} Tfstate object
|
|
36298
36829
|
*
|
|
36299
36830
|
* @example
|
|
36300
36831
|
* // Export latest state
|
|
@@ -37644,6 +38175,533 @@ class VectorPlugin extends Plugin {
|
|
|
37644
38175
|
}
|
|
37645
38176
|
}
|
|
37646
38177
|
|
|
38178
|
+
function mapFieldTypeToTypeScript(fieldType) {
|
|
38179
|
+
const baseType = fieldType.split("|")[0].trim();
|
|
38180
|
+
const typeMap = {
|
|
38181
|
+
"string": "string",
|
|
38182
|
+
"number": "number",
|
|
38183
|
+
"integer": "number",
|
|
38184
|
+
"boolean": "boolean",
|
|
38185
|
+
"array": "any[]",
|
|
38186
|
+
"object": "Record<string, any>",
|
|
38187
|
+
"json": "Record<string, any>",
|
|
38188
|
+
"secret": "string",
|
|
38189
|
+
"email": "string",
|
|
38190
|
+
"url": "string",
|
|
38191
|
+
"date": "string",
|
|
38192
|
+
// ISO date string
|
|
38193
|
+
"datetime": "string",
|
|
38194
|
+
// ISO datetime string
|
|
38195
|
+
"ip4": "string",
|
|
38196
|
+
"ip6": "string"
|
|
38197
|
+
};
|
|
38198
|
+
if (baseType.startsWith("embedding:")) {
|
|
38199
|
+
const dimensions = parseInt(baseType.split(":")[1]);
|
|
38200
|
+
return `number[] /* ${dimensions} dimensions */`;
|
|
38201
|
+
}
|
|
38202
|
+
return typeMap[baseType] || "any";
|
|
38203
|
+
}
|
|
38204
|
+
function isFieldRequired(fieldDef) {
|
|
38205
|
+
if (typeof fieldDef === "string") {
|
|
38206
|
+
return fieldDef.includes("|required");
|
|
38207
|
+
}
|
|
38208
|
+
if (typeof fieldDef === "object" && fieldDef.required) {
|
|
38209
|
+
return true;
|
|
38210
|
+
}
|
|
38211
|
+
return false;
|
|
38212
|
+
}
|
|
38213
|
+
function generateResourceInterface(resourceName, attributes, timestamps = false) {
|
|
38214
|
+
const interfaceName = toPascalCase(resourceName);
|
|
38215
|
+
const lines = [];
|
|
38216
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
38217
|
+
lines.push(` /** Resource ID (auto-generated) */`);
|
|
38218
|
+
lines.push(` id: string;`);
|
|
38219
|
+
lines.push("");
|
|
38220
|
+
for (const [fieldName, fieldDef] of Object.entries(attributes)) {
|
|
38221
|
+
const required = isFieldRequired(fieldDef);
|
|
38222
|
+
const optional = required ? "" : "?";
|
|
38223
|
+
let tsType;
|
|
38224
|
+
if (typeof fieldDef === "string") {
|
|
38225
|
+
tsType = mapFieldTypeToTypeScript(fieldDef);
|
|
38226
|
+
} else if (typeof fieldDef === "object" && fieldDef.type) {
|
|
38227
|
+
tsType = mapFieldTypeToTypeScript(fieldDef.type);
|
|
38228
|
+
if (fieldDef.type === "object" && fieldDef.props) {
|
|
38229
|
+
tsType = "{\n";
|
|
38230
|
+
for (const [propName, propDef] of Object.entries(fieldDef.props)) {
|
|
38231
|
+
const propType = typeof propDef === "string" ? mapFieldTypeToTypeScript(propDef) : mapFieldTypeToTypeScript(propDef.type);
|
|
38232
|
+
const propRequired = isFieldRequired(propDef);
|
|
38233
|
+
tsType += ` ${propName}${propRequired ? "" : "?"}: ${propType};
|
|
38234
|
+
`;
|
|
38235
|
+
}
|
|
38236
|
+
tsType += " }";
|
|
38237
|
+
}
|
|
38238
|
+
if (fieldDef.type === "array" && fieldDef.items) {
|
|
38239
|
+
const itemType = mapFieldTypeToTypeScript(fieldDef.items);
|
|
38240
|
+
tsType = `Array<${itemType}>`;
|
|
38241
|
+
}
|
|
38242
|
+
} else {
|
|
38243
|
+
tsType = "any";
|
|
38244
|
+
}
|
|
38245
|
+
if (fieldDef.description) {
|
|
38246
|
+
lines.push(` /** ${fieldDef.description} */`);
|
|
38247
|
+
}
|
|
38248
|
+
lines.push(` ${fieldName}${optional}: ${tsType};`);
|
|
38249
|
+
}
|
|
38250
|
+
if (timestamps) {
|
|
38251
|
+
lines.push("");
|
|
38252
|
+
lines.push(` /** Creation timestamp (ISO 8601) */`);
|
|
38253
|
+
lines.push(` createdAt: string;`);
|
|
38254
|
+
lines.push(` /** Last update timestamp (ISO 8601) */`);
|
|
38255
|
+
lines.push(` updatedAt: string;`);
|
|
38256
|
+
}
|
|
38257
|
+
lines.push("}");
|
|
38258
|
+
lines.push("");
|
|
38259
|
+
return lines.join("\n");
|
|
38260
|
+
}
|
|
38261
|
+
function toPascalCase(str) {
|
|
38262
|
+
return str.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
38263
|
+
}
|
|
38264
|
+
async function generateTypes(database, options = {}) {
|
|
38265
|
+
const {
|
|
38266
|
+
outputPath = "./types/database.d.ts",
|
|
38267
|
+
moduleName = "s3db.js",
|
|
38268
|
+
includeResource = true
|
|
38269
|
+
} = options;
|
|
38270
|
+
const lines = [];
|
|
38271
|
+
lines.push("/**");
|
|
38272
|
+
lines.push(" * Auto-generated TypeScript definitions for s3db.js resources");
|
|
38273
|
+
lines.push(" * Generated at: " + (/* @__PURE__ */ new Date()).toISOString());
|
|
38274
|
+
lines.push(" * DO NOT EDIT - This file is auto-generated");
|
|
38275
|
+
lines.push(" */");
|
|
38276
|
+
lines.push("");
|
|
38277
|
+
if (includeResource) {
|
|
38278
|
+
lines.push(`import { Resource, Database } from '${moduleName}';`);
|
|
38279
|
+
lines.push("");
|
|
38280
|
+
}
|
|
38281
|
+
const resourceInterfaces = [];
|
|
38282
|
+
for (const [name, resource] of Object.entries(database.resources)) {
|
|
38283
|
+
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
38284
|
+
const timestamps = resource.config?.timestamps || false;
|
|
38285
|
+
const interfaceDef = generateResourceInterface(name, attributes, timestamps);
|
|
38286
|
+
lines.push(interfaceDef);
|
|
38287
|
+
resourceInterfaces.push({
|
|
38288
|
+
name,
|
|
38289
|
+
interfaceName: toPascalCase(name),
|
|
38290
|
+
resource
|
|
38291
|
+
});
|
|
38292
|
+
}
|
|
38293
|
+
lines.push("/**");
|
|
38294
|
+
lines.push(" * Typed resource map for property access");
|
|
38295
|
+
lines.push(" * @example");
|
|
38296
|
+
lines.push(" * const users = db.resources.users; // Type-safe!");
|
|
38297
|
+
lines.push(' * const user = await users.get("id"); // Autocomplete works!');
|
|
38298
|
+
lines.push(" */");
|
|
38299
|
+
lines.push("export interface ResourceMap {");
|
|
38300
|
+
for (const { name, interfaceName } of resourceInterfaces) {
|
|
38301
|
+
lines.push(` /** ${interfaceName} resource */`);
|
|
38302
|
+
if (includeResource) {
|
|
38303
|
+
lines.push(` ${name}: Resource<${interfaceName}>;`);
|
|
38304
|
+
} else {
|
|
38305
|
+
lines.push(` ${name}: any;`);
|
|
38306
|
+
}
|
|
38307
|
+
}
|
|
38308
|
+
lines.push("}");
|
|
38309
|
+
lines.push("");
|
|
38310
|
+
if (includeResource) {
|
|
38311
|
+
lines.push("/**");
|
|
38312
|
+
lines.push(" * Extended Database class with typed resources");
|
|
38313
|
+
lines.push(" */");
|
|
38314
|
+
lines.push("declare module 's3db.js' {");
|
|
38315
|
+
lines.push(" interface Database {");
|
|
38316
|
+
lines.push(" resources: ResourceMap;");
|
|
38317
|
+
lines.push(" }");
|
|
38318
|
+
lines.push("");
|
|
38319
|
+
lines.push(" interface Resource<T = any> {");
|
|
38320
|
+
lines.push(" get(id: string): Promise<T>;");
|
|
38321
|
+
lines.push(" getOrNull(id: string): Promise<T | null>;");
|
|
38322
|
+
lines.push(" getOrThrow(id: string): Promise<T>;");
|
|
38323
|
+
lines.push(" insert(data: Partial<T>): Promise<T>;");
|
|
38324
|
+
lines.push(" update(id: string, data: Partial<T>): Promise<T>;");
|
|
38325
|
+
lines.push(" patch(id: string, data: Partial<T>): Promise<T>;");
|
|
38326
|
+
lines.push(" replace(id: string, data: Partial<T>): Promise<T>;");
|
|
38327
|
+
lines.push(" delete(id: string): Promise<void>;");
|
|
38328
|
+
lines.push(" list(options?: any): Promise<T[]>;");
|
|
38329
|
+
lines.push(" query(filters: Partial<T>, options?: any): Promise<T[]>;");
|
|
38330
|
+
lines.push(" validate(data: Partial<T>, options?: any): Promise<{ valid: boolean; errors: any[]; data: T | null }>;");
|
|
38331
|
+
lines.push(" }");
|
|
38332
|
+
lines.push("}");
|
|
38333
|
+
}
|
|
38334
|
+
const content = lines.join("\n");
|
|
38335
|
+
if (outputPath) {
|
|
38336
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
38337
|
+
await writeFile(outputPath, content, "utf-8");
|
|
38338
|
+
}
|
|
38339
|
+
return content;
|
|
38340
|
+
}
|
|
38341
|
+
async function printTypes(database, options = {}) {
|
|
38342
|
+
const types = await generateTypes(database, { ...options, outputPath: null });
|
|
38343
|
+
console.log(types);
|
|
38344
|
+
return types;
|
|
38345
|
+
}
|
|
38346
|
+
|
|
38347
|
+
class Factory {
|
|
38348
|
+
/**
|
|
38349
|
+
* Global sequence counter
|
|
38350
|
+
* @private
|
|
38351
|
+
*/
|
|
38352
|
+
static _sequences = /* @__PURE__ */ new Map();
|
|
38353
|
+
/**
|
|
38354
|
+
* Registered factories
|
|
38355
|
+
* @private
|
|
38356
|
+
*/
|
|
38357
|
+
static _factories = /* @__PURE__ */ new Map();
|
|
38358
|
+
/**
|
|
38359
|
+
* Database instance (set globally)
|
|
38360
|
+
* @private
|
|
38361
|
+
*/
|
|
38362
|
+
static _database = null;
|
|
38363
|
+
/**
|
|
38364
|
+
* Create a new factory definition
|
|
38365
|
+
* @param {string} resourceName - Resource name
|
|
38366
|
+
* @param {Object|Function} definition - Field definitions or function
|
|
38367
|
+
* @param {Object} options - Factory options
|
|
38368
|
+
* @returns {Factory} Factory instance
|
|
38369
|
+
*/
|
|
38370
|
+
static define(resourceName, definition, options = {}) {
|
|
38371
|
+
const factory = new Factory(resourceName, definition, options);
|
|
38372
|
+
Factory._factories.set(resourceName, factory);
|
|
38373
|
+
return factory;
|
|
38374
|
+
}
|
|
38375
|
+
/**
|
|
38376
|
+
* Set global database instance
|
|
38377
|
+
* @param {Database} database - s3db.js Database instance
|
|
38378
|
+
*/
|
|
38379
|
+
static setDatabase(database) {
|
|
38380
|
+
Factory._database = database;
|
|
38381
|
+
}
|
|
38382
|
+
/**
|
|
38383
|
+
* Get factory by resource name
|
|
38384
|
+
* @param {string} resourceName - Resource name
|
|
38385
|
+
* @returns {Factory} Factory instance
|
|
38386
|
+
*/
|
|
38387
|
+
static get(resourceName) {
|
|
38388
|
+
return Factory._factories.get(resourceName);
|
|
38389
|
+
}
|
|
38390
|
+
/**
|
|
38391
|
+
* Reset all sequences
|
|
38392
|
+
*/
|
|
38393
|
+
static resetSequences() {
|
|
38394
|
+
Factory._sequences.clear();
|
|
38395
|
+
}
|
|
38396
|
+
/**
|
|
38397
|
+
* Reset all factories
|
|
38398
|
+
*/
|
|
38399
|
+
static reset() {
|
|
38400
|
+
Factory._sequences.clear();
|
|
38401
|
+
Factory._factories.clear();
|
|
38402
|
+
Factory._database = null;
|
|
38403
|
+
}
|
|
38404
|
+
/**
|
|
38405
|
+
* Constructor
|
|
38406
|
+
* @param {string} resourceName - Resource name
|
|
38407
|
+
* @param {Object|Function} definition - Field definitions
|
|
38408
|
+
* @param {Object} options - Factory options
|
|
38409
|
+
*/
|
|
38410
|
+
constructor(resourceName, definition, options = {}) {
|
|
38411
|
+
this.resourceName = resourceName;
|
|
38412
|
+
this.definition = definition;
|
|
38413
|
+
this.options = options;
|
|
38414
|
+
this.traits = /* @__PURE__ */ new Map();
|
|
38415
|
+
this.afterCreateCallbacks = [];
|
|
38416
|
+
this.beforeCreateCallbacks = [];
|
|
38417
|
+
}
|
|
38418
|
+
/**
|
|
38419
|
+
* Get next sequence number
|
|
38420
|
+
* @param {string} name - Sequence name (default: factory name)
|
|
38421
|
+
* @returns {number} Next sequence number
|
|
38422
|
+
*/
|
|
38423
|
+
sequence(name = this.resourceName) {
|
|
38424
|
+
const current = Factory._sequences.get(name) || 0;
|
|
38425
|
+
const next = current + 1;
|
|
38426
|
+
Factory._sequences.set(name, next);
|
|
38427
|
+
return next;
|
|
38428
|
+
}
|
|
38429
|
+
/**
|
|
38430
|
+
* Define a trait (state variation)
|
|
38431
|
+
* @param {string} name - Trait name
|
|
38432
|
+
* @param {Object|Function} attributes - Trait attributes
|
|
38433
|
+
* @returns {Factory} This factory (for chaining)
|
|
38434
|
+
*/
|
|
38435
|
+
trait(name, attributes) {
|
|
38436
|
+
this.traits.set(name, attributes);
|
|
38437
|
+
return this;
|
|
38438
|
+
}
|
|
38439
|
+
/**
|
|
38440
|
+
* Register after create callback
|
|
38441
|
+
* @param {Function} callback - Callback function
|
|
38442
|
+
* @returns {Factory} This factory (for chaining)
|
|
38443
|
+
*/
|
|
38444
|
+
afterCreate(callback) {
|
|
38445
|
+
this.afterCreateCallbacks.push(callback);
|
|
38446
|
+
return this;
|
|
38447
|
+
}
|
|
38448
|
+
/**
|
|
38449
|
+
* Register before create callback
|
|
38450
|
+
* @param {Function} callback - Callback function
|
|
38451
|
+
* @returns {Factory} This factory (for chaining)
|
|
38452
|
+
*/
|
|
38453
|
+
beforeCreate(callback) {
|
|
38454
|
+
this.beforeCreateCallbacks.push(callback);
|
|
38455
|
+
return this;
|
|
38456
|
+
}
|
|
38457
|
+
/**
|
|
38458
|
+
* Build attributes without creating in database
|
|
38459
|
+
* @param {Object} overrides - Override attributes
|
|
38460
|
+
* @param {Object} options - Build options
|
|
38461
|
+
* @returns {Promise<Object>} Built attributes
|
|
38462
|
+
*/
|
|
38463
|
+
async build(overrides = {}, options = {}) {
|
|
38464
|
+
const { traits = [] } = options;
|
|
38465
|
+
const seq = this.sequence();
|
|
38466
|
+
let attributes = typeof this.definition === "function" ? await this.definition({ seq, factory: this }) : { ...this.definition };
|
|
38467
|
+
for (const traitName of traits) {
|
|
38468
|
+
const trait = this.traits.get(traitName);
|
|
38469
|
+
if (!trait) {
|
|
38470
|
+
throw new Error(`Trait '${traitName}' not found in factory '${this.resourceName}'`);
|
|
38471
|
+
}
|
|
38472
|
+
const traitAttrs = typeof trait === "function" ? await trait({ seq, factory: this }) : trait;
|
|
38473
|
+
attributes = { ...attributes, ...traitAttrs };
|
|
38474
|
+
}
|
|
38475
|
+
attributes = { ...attributes, ...overrides };
|
|
38476
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
38477
|
+
if (typeof value === "function") {
|
|
38478
|
+
attributes[key] = await value({ seq, factory: this });
|
|
38479
|
+
}
|
|
38480
|
+
}
|
|
38481
|
+
return attributes;
|
|
38482
|
+
}
|
|
38483
|
+
/**
|
|
38484
|
+
* Create resource in database
|
|
38485
|
+
* @param {Object} overrides - Override attributes
|
|
38486
|
+
* @param {Object} options - Create options
|
|
38487
|
+
* @returns {Promise<Object>} Created resource
|
|
38488
|
+
*/
|
|
38489
|
+
async create(overrides = {}, options = {}) {
|
|
38490
|
+
const { database = Factory._database } = options;
|
|
38491
|
+
if (!database) {
|
|
38492
|
+
throw new Error("Database not set. Use Factory.setDatabase(db) or pass database option");
|
|
38493
|
+
}
|
|
38494
|
+
let attributes = await this.build(overrides, options);
|
|
38495
|
+
for (const callback of this.beforeCreateCallbacks) {
|
|
38496
|
+
attributes = await callback(attributes) || attributes;
|
|
38497
|
+
}
|
|
38498
|
+
const resource = database.resources[this.resourceName];
|
|
38499
|
+
if (!resource) {
|
|
38500
|
+
throw new Error(`Resource '${this.resourceName}' not found in database`);
|
|
38501
|
+
}
|
|
38502
|
+
let created = await resource.insert(attributes);
|
|
38503
|
+
for (const callback of this.afterCreateCallbacks) {
|
|
38504
|
+
created = await callback(created, { database }) || created;
|
|
38505
|
+
}
|
|
38506
|
+
return created;
|
|
38507
|
+
}
|
|
38508
|
+
/**
|
|
38509
|
+
* Create multiple resources
|
|
38510
|
+
* @param {number} count - Number of resources to create
|
|
38511
|
+
* @param {Object} overrides - Override attributes
|
|
38512
|
+
* @param {Object} options - Create options
|
|
38513
|
+
* @returns {Promise<Object[]>} Created resources
|
|
38514
|
+
*/
|
|
38515
|
+
async createMany(count, overrides = {}, options = {}) {
|
|
38516
|
+
const resources = [];
|
|
38517
|
+
for (let i = 0; i < count; i++) {
|
|
38518
|
+
const resource = await this.create(overrides, options);
|
|
38519
|
+
resources.push(resource);
|
|
38520
|
+
}
|
|
38521
|
+
return resources;
|
|
38522
|
+
}
|
|
38523
|
+
/**
|
|
38524
|
+
* Build multiple resources without creating
|
|
38525
|
+
* @param {number} count - Number of resources to build
|
|
38526
|
+
* @param {Object} overrides - Override attributes
|
|
38527
|
+
* @param {Object} options - Build options
|
|
38528
|
+
* @returns {Promise<Object[]>} Built resources
|
|
38529
|
+
*/
|
|
38530
|
+
async buildMany(count, overrides = {}, options = {}) {
|
|
38531
|
+
const resources = [];
|
|
38532
|
+
for (let i = 0; i < count; i++) {
|
|
38533
|
+
const resource = await this.build(overrides, options);
|
|
38534
|
+
resources.push(resource);
|
|
38535
|
+
}
|
|
38536
|
+
return resources;
|
|
38537
|
+
}
|
|
38538
|
+
/**
|
|
38539
|
+
* Create with specific traits
|
|
38540
|
+
* @param {string|string[]} traits - Trait name(s)
|
|
38541
|
+
* @param {Object} overrides - Override attributes
|
|
38542
|
+
* @param {Object} options - Create options
|
|
38543
|
+
* @returns {Promise<Object>} Created resource
|
|
38544
|
+
*/
|
|
38545
|
+
async createWithTraits(traits, overrides = {}, options = {}) {
|
|
38546
|
+
const traitArray = Array.isArray(traits) ? traits : [traits];
|
|
38547
|
+
return this.create(overrides, { ...options, traits: traitArray });
|
|
38548
|
+
}
|
|
38549
|
+
/**
|
|
38550
|
+
* Build with specific traits
|
|
38551
|
+
* @param {string|string[]} traits - Trait name(s)
|
|
38552
|
+
* @param {Object} overrides - Override attributes
|
|
38553
|
+
* @param {Object} options - Build options
|
|
38554
|
+
* @returns {Promise<Object>} Built resource
|
|
38555
|
+
*/
|
|
38556
|
+
async buildWithTraits(traits, overrides = {}, options = {}) {
|
|
38557
|
+
const traitArray = Array.isArray(traits) ? traits : [traits];
|
|
38558
|
+
return this.build(overrides, { ...options, traits: traitArray });
|
|
38559
|
+
}
|
|
38560
|
+
}
|
|
38561
|
+
|
|
38562
|
+
class Seeder {
|
|
38563
|
+
/**
|
|
38564
|
+
* Constructor
|
|
38565
|
+
* @param {Database} database - s3db.js Database instance
|
|
38566
|
+
* @param {Object} options - Seeder options
|
|
38567
|
+
*/
|
|
38568
|
+
constructor(database, options = {}) {
|
|
38569
|
+
this.database = database;
|
|
38570
|
+
this.options = options;
|
|
38571
|
+
this.verbose = options.verbose !== false;
|
|
38572
|
+
}
|
|
38573
|
+
/**
|
|
38574
|
+
* Log message (if verbose)
|
|
38575
|
+
* @param {string} message - Message to log
|
|
38576
|
+
* @private
|
|
38577
|
+
*/
|
|
38578
|
+
log(message) {
|
|
38579
|
+
if (this.verbose) {
|
|
38580
|
+
console.log(`[Seeder] ${message}`);
|
|
38581
|
+
}
|
|
38582
|
+
}
|
|
38583
|
+
/**
|
|
38584
|
+
* Seed resources using factories
|
|
38585
|
+
* @param {Object} specs - Seed specifications { resourceName: count }
|
|
38586
|
+
* @returns {Promise<Object>} Created resources by resource name
|
|
38587
|
+
*
|
|
38588
|
+
* @example
|
|
38589
|
+
* const created = await seeder.seed({
|
|
38590
|
+
* users: 10,
|
|
38591
|
+
* posts: 50
|
|
38592
|
+
* });
|
|
38593
|
+
*/
|
|
38594
|
+
async seed(specs) {
|
|
38595
|
+
const created = {};
|
|
38596
|
+
for (const [resourceName, count] of Object.entries(specs)) {
|
|
38597
|
+
this.log(`Seeding ${count} ${resourceName}...`);
|
|
38598
|
+
const factory = Factory.get(resourceName);
|
|
38599
|
+
if (!factory) {
|
|
38600
|
+
throw new Error(`Factory for '${resourceName}' not found. Define it with Factory.define()`);
|
|
38601
|
+
}
|
|
38602
|
+
created[resourceName] = await factory.createMany(count, {}, { database: this.database });
|
|
38603
|
+
this.log(`\u2705 Created ${count} ${resourceName}`);
|
|
38604
|
+
}
|
|
38605
|
+
return created;
|
|
38606
|
+
}
|
|
38607
|
+
/**
|
|
38608
|
+
* Seed with custom callback
|
|
38609
|
+
* @param {Function} callback - Seeding callback
|
|
38610
|
+
* @returns {Promise<any>} Result of callback
|
|
38611
|
+
*
|
|
38612
|
+
* @example
|
|
38613
|
+
* await seeder.call(async (db) => {
|
|
38614
|
+
* const user = await UserFactory.create();
|
|
38615
|
+
* const posts = await PostFactory.createMany(5, { userId: user.id });
|
|
38616
|
+
* return { user, posts };
|
|
38617
|
+
* });
|
|
38618
|
+
*/
|
|
38619
|
+
async call(callback) {
|
|
38620
|
+
this.log("Running custom seeder...");
|
|
38621
|
+
const result = await callback(this.database);
|
|
38622
|
+
this.log("\u2705 Custom seeder completed");
|
|
38623
|
+
return result;
|
|
38624
|
+
}
|
|
38625
|
+
/**
|
|
38626
|
+
* Truncate resources (delete all data)
|
|
38627
|
+
* @param {string[]} resourceNames - Resource names to truncate
|
|
38628
|
+
* @returns {Promise<void>}
|
|
38629
|
+
*
|
|
38630
|
+
* @example
|
|
38631
|
+
* await seeder.truncate(['users', 'posts']);
|
|
38632
|
+
*/
|
|
38633
|
+
async truncate(resourceNames) {
|
|
38634
|
+
for (const resourceName of resourceNames) {
|
|
38635
|
+
this.log(`Truncating ${resourceName}...`);
|
|
38636
|
+
const resource = this.database.resources[resourceName];
|
|
38637
|
+
if (!resource) {
|
|
38638
|
+
this.log(`\u26A0\uFE0F Resource '${resourceName}' not found, skipping`);
|
|
38639
|
+
continue;
|
|
38640
|
+
}
|
|
38641
|
+
const ids = await resource.listIds();
|
|
38642
|
+
if (ids.length > 0) {
|
|
38643
|
+
await resource.deleteMany(ids);
|
|
38644
|
+
this.log(`\u2705 Deleted ${ids.length} ${resourceName}`);
|
|
38645
|
+
} else {
|
|
38646
|
+
this.log(`\u2705 ${resourceName} already empty`);
|
|
38647
|
+
}
|
|
38648
|
+
}
|
|
38649
|
+
}
|
|
38650
|
+
/**
|
|
38651
|
+
* Truncate all resources
|
|
38652
|
+
* @returns {Promise<void>}
|
|
38653
|
+
*/
|
|
38654
|
+
async truncateAll() {
|
|
38655
|
+
const resourceNames = Object.keys(this.database.resources);
|
|
38656
|
+
await this.truncate(resourceNames);
|
|
38657
|
+
}
|
|
38658
|
+
/**
|
|
38659
|
+
* Run multiple seeders in order
|
|
38660
|
+
* @param {Function[]} seeders - Array of seeder functions
|
|
38661
|
+
* @returns {Promise<Object[]>} Results of each seeder
|
|
38662
|
+
*
|
|
38663
|
+
* @example
|
|
38664
|
+
* await seeder.run([
|
|
38665
|
+
* async (db) => await UserFactory.createMany(10),
|
|
38666
|
+
* async (db) => await PostFactory.createMany(50)
|
|
38667
|
+
* ]);
|
|
38668
|
+
*/
|
|
38669
|
+
async run(seeders) {
|
|
38670
|
+
const results = [];
|
|
38671
|
+
for (const seederFn of seeders) {
|
|
38672
|
+
this.log(`Running seeder ${seederFn.name || "anonymous"}...`);
|
|
38673
|
+
const result = await seederFn(this.database);
|
|
38674
|
+
results.push(result);
|
|
38675
|
+
this.log(`\u2705 Completed ${seederFn.name || "anonymous"}`);
|
|
38676
|
+
}
|
|
38677
|
+
return results;
|
|
38678
|
+
}
|
|
38679
|
+
/**
|
|
38680
|
+
* Seed and return specific resources
|
|
38681
|
+
* @param {Object} specs - Seed specifications
|
|
38682
|
+
* @returns {Promise<Object>} Created resources
|
|
38683
|
+
*
|
|
38684
|
+
* @example
|
|
38685
|
+
* const { users, posts } = await seeder.seedAndReturn({
|
|
38686
|
+
* users: 5,
|
|
38687
|
+
* posts: 10
|
|
38688
|
+
* });
|
|
38689
|
+
*/
|
|
38690
|
+
async seedAndReturn(specs) {
|
|
38691
|
+
return await this.seed(specs);
|
|
38692
|
+
}
|
|
38693
|
+
/**
|
|
38694
|
+
* Reset database (truncate all and reset sequences)
|
|
38695
|
+
* @returns {Promise<void>}
|
|
38696
|
+
*/
|
|
38697
|
+
async reset() {
|
|
38698
|
+
this.log("Resetting database...");
|
|
38699
|
+
await this.truncateAll();
|
|
38700
|
+
Factory.resetSequences();
|
|
38701
|
+
this.log("\u2705 Database reset complete");
|
|
38702
|
+
}
|
|
38703
|
+
}
|
|
38704
|
+
|
|
37647
38705
|
function sanitizeLabel(value) {
|
|
37648
38706
|
if (typeof value !== "string") {
|
|
37649
38707
|
value = String(value);
|
|
@@ -38018,5 +39076,5 @@ var metrics = /*#__PURE__*/Object.freeze({
|
|
|
38018
39076
|
silhouetteScore: silhouetteScore
|
|
38019
39077
|
});
|
|
38020
39078
|
|
|
38021
|
-
export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, ApiPlugin, AuditPlugin, AuthenticationError, BACKUP_DRIVERS, BackupPlugin, BaseBackupDriver, BaseError, BaseReplicator, BehaviorError, BigqueryReplicator, CONSUMER_DRIVERS, Cache, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, DynamoDBReplicator, EncryptionError, ErrorMap, EventualConsistencyPlugin, FilesystemBackupDriver, FilesystemCache, FullTextPlugin, InvalidResourceItem, MemoryCache, MetadataLimitError, MetricsPlugin, MissingMetadata, MongoDBReplicator, MultiBackupDriver, MySQLReplicator, NoSuchBucket, NoSuchKey, NotFound, PartitionAwareFilesystemCache, PartitionDriverError, PartitionError, PermissionError, PlanetScaleReplicator, Plugin, PluginError, PluginObject, PluginStorageError, PostgresReplicator, QueueConsumerPlugin, REPLICATOR_DRIVERS, RabbitMqConsumer, RelationPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3BackupDriver, S3Cache, S3QueuePlugin, Database as S3db, S3dbError, S3dbReplicator, SchedulerPlugin, Schema, SchemaError, SqsConsumer, SqsReplicator, StateMachinePlugin, StreamError, TfStatePlugin, TursoReplicator, UnknownError, ValidationError, Validator, VectorPlugin, WebhookReplicator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes,
|
|
39079
|
+
export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, ApiPlugin, AuditPlugin, AuthenticationError, BACKUP_DRIVERS, BackupPlugin, BaseBackupDriver, BaseError, BaseReplicator, BehaviorError, BigqueryReplicator, CONSUMER_DRIVERS, Cache, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, DynamoDBReplicator, EncryptionError, ErrorMap, EventualConsistencyPlugin, Factory, FilesystemBackupDriver, FilesystemCache, FullTextPlugin, InvalidResourceItem, MemoryCache, MetadataLimitError, MetricsPlugin, MissingMetadata, MongoDBReplicator, MultiBackupDriver, MySQLReplicator, NoSuchBucket, NoSuchKey, NotFound, PartitionAwareFilesystemCache, PartitionDriverError, PartitionError, PermissionError, PlanetScaleReplicator, Plugin, PluginError, PluginObject, PluginStorageError, PostgresReplicator, QueueConsumerPlugin, REPLICATOR_DRIVERS, RabbitMqConsumer, RelationPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3BackupDriver, S3Cache, S3QueuePlugin, Database as S3db, S3dbError, S3dbReplicator, SchedulerPlugin, Schema, SchemaError, Seeder, SqsConsumer, SqsReplicator, StateMachinePlugin, StreamError, TfStatePlugin, TursoReplicator, UnknownError, ValidationError, Validator, VectorPlugin, WebhookReplicator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes, clearUTF8Memory, createBackupDriver, createConsumer, createReplicator, decode, decodeDecimal, decodeFixedPoint, decodeFixedPointBatch, decrypt, S3db as default, encode, encodeDecimal, encodeFixedPoint, encodeFixedPointBatch, encrypt, generateTypes, getBehavior, getSizeBreakdown, idGenerator, mapAwsError, md5, passwordGenerator, printTypes, sha256, streamToString, transformValue, tryFn, tryFnSync, validateBackupConfig, validateReplicatorConfig };
|
|
38022
39080
|
//# sourceMappingURL=s3db.es.js.map
|