s3db.js 12.2.4 → 12.3.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/dist/s3db.cjs.js +329 -43
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +329 -43
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/body-only.js +15 -5
- package/src/behaviors/body-overflow.js +9 -0
- package/src/behaviors/user-managed.js +8 -1
- package/src/concerns/typescript-generator.js +12 -2
- package/src/plugins/api/utils/openapi-generator.js +21 -2
- package/src/plugins/replicators/bigquery-replicator.class.js +9 -1
- package/src/plugins/replicators/mysql-replicator.class.js +9 -1
- package/src/plugins/replicators/planetscale-replicator.class.js +9 -1
- package/src/plugins/replicators/postgres-replicator.class.js +9 -1
- package/src/plugins/replicators/schema-sync.helper.js +19 -0
- package/src/plugins/replicators/turso-replicator.class.js +9 -1
- package/src/plugins/vector.plugin.js +3 -3
- package/src/resource.class.js +203 -4
- package/src/schema.class.js +223 -33
package/package.json
CHANGED
|
@@ -59,10 +59,15 @@ export async function handleInsert({ resource, data, mappedData }) {
|
|
|
59
59
|
'_v': mappedData._v || String(resource.version)
|
|
60
60
|
};
|
|
61
61
|
metadataOnly._map = JSON.stringify(resource.schema.map);
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
// Store pluginMap for backwards compatibility when plugins are added/removed
|
|
64
|
+
if (resource.schema.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
65
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
// Use the original object for the body
|
|
64
69
|
const body = JSON.stringify(mappedData);
|
|
65
|
-
|
|
70
|
+
|
|
66
71
|
return { mappedData: metadataOnly, body };
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -70,16 +75,21 @@ export async function handleUpdate({ resource, id, data, mappedData }) {
|
|
|
70
75
|
// For updates, we need to merge with existing data
|
|
71
76
|
// Since we can't easily read the existing body during update,
|
|
72
77
|
// we'll put the update data in the body and let the resource handle merging
|
|
73
|
-
|
|
78
|
+
|
|
74
79
|
// Keep only the version field in metadata
|
|
75
80
|
const metadataOnly = {
|
|
76
81
|
'_v': mappedData._v || String(resource.version)
|
|
77
82
|
};
|
|
78
83
|
metadataOnly._map = JSON.stringify(resource.schema.map);
|
|
79
|
-
|
|
84
|
+
|
|
85
|
+
// Store pluginMap for backwards compatibility when plugins are added/removed
|
|
86
|
+
if (resource.schema.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
87
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
// Use the original object for the body
|
|
81
91
|
const body = JSON.stringify(mappedData);
|
|
82
|
-
|
|
92
|
+
|
|
83
93
|
return { mappedData: metadataOnly, body };
|
|
84
94
|
}
|
|
85
95
|
|
|
@@ -90,6 +90,15 @@ export async function handleInsert({ resource, data, mappedData, originalData })
|
|
|
90
90
|
currentSize += attributeSizes._v;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// Always include plugin map for backwards compatibility when plugins are added/removed
|
|
94
|
+
// Note: _pluginMap is not in mappedData, it's added separately by behaviors
|
|
95
|
+
if (resource.schema?.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
96
|
+
const pluginMapStr = JSON.stringify(resource.schema.pluginMap);
|
|
97
|
+
const pluginMapSize = calculateUTF8Bytes('_pluginMap') + calculateUTF8Bytes(pluginMapStr);
|
|
98
|
+
metadataFields._pluginMap = pluginMapStr;
|
|
99
|
+
currentSize += pluginMapSize;
|
|
100
|
+
}
|
|
101
|
+
|
|
93
102
|
// Reserve space for $overflow if overflow is possible
|
|
94
103
|
let reservedLimit = effectiveLimit;
|
|
95
104
|
for (const [fieldName, size] of sortedFields) {
|
|
@@ -88,7 +88,14 @@ export async function handleInsert({ resource, data, mappedData, originalData })
|
|
|
88
88
|
data: originalData || data
|
|
89
89
|
});
|
|
90
90
|
// If data exceeds limit, store in body
|
|
91
|
-
|
|
91
|
+
const metadataOnly = { _v: mappedData._v };
|
|
92
|
+
|
|
93
|
+
// Store pluginMap for backwards compatibility when plugins are added/removed
|
|
94
|
+
if (resource.schema?.pluginMap && Object.keys(resource.schema.pluginMap).length > 0) {
|
|
95
|
+
metadataOnly._pluginMap = JSON.stringify(resource.schema.pluginMap);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { mappedData: metadataOnly, body: JSON.stringify(mappedData) };
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
// If data fits in metadata, store only in metadata
|
|
@@ -190,10 +190,20 @@ export async function generateTypes(database, options = {}) {
|
|
|
190
190
|
const resourceInterfaces = [];
|
|
191
191
|
|
|
192
192
|
for (const [name, resource] of Object.entries(database.resources)) {
|
|
193
|
-
const
|
|
193
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
194
194
|
const timestamps = resource.config?.timestamps || false;
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
// Filter out plugin attributes - they are internal implementation details
|
|
197
|
+
// and should not be exposed in public TypeScript interfaces
|
|
198
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
199
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
200
|
+
: [];
|
|
201
|
+
|
|
202
|
+
const userAttributes = Object.fromEntries(
|
|
203
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const interfaceDef = generateResourceInterface(name, userAttributes, timestamps);
|
|
197
207
|
lines.push(interfaceDef);
|
|
198
208
|
|
|
199
209
|
resourceInterfaces.push({
|
|
@@ -98,7 +98,17 @@ function generateResourceSchema(resource) {
|
|
|
98
98
|
const properties = {};
|
|
99
99
|
const required = [];
|
|
100
100
|
|
|
101
|
-
const
|
|
101
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
102
|
+
|
|
103
|
+
// Filter out plugin attributes - they are internal implementation details
|
|
104
|
+
// and should not be exposed in public API documentation
|
|
105
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
106
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
107
|
+
: [];
|
|
108
|
+
|
|
109
|
+
const attributes = Object.fromEntries(
|
|
110
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
111
|
+
);
|
|
102
112
|
|
|
103
113
|
// Extract resource description (supports both string and object format)
|
|
104
114
|
const resourceDescription = resource.config?.description;
|
|
@@ -254,7 +264,16 @@ function generateResourcePaths(resource, version, config = {}) {
|
|
|
254
264
|
}
|
|
255
265
|
|
|
256
266
|
// Create query parameters only for partition fields
|
|
257
|
-
const
|
|
267
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
268
|
+
|
|
269
|
+
// Filter out plugin attributes
|
|
270
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
271
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
272
|
+
: [];
|
|
273
|
+
|
|
274
|
+
const attributes = Object.fromEntries(
|
|
275
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
276
|
+
);
|
|
258
277
|
|
|
259
278
|
for (const fieldName of partitionFieldsSet) {
|
|
260
279
|
const fieldDef = attributes[fieldName];
|
|
@@ -186,7 +186,15 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
186
186
|
continue;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
const
|
|
189
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
190
|
+
|
|
191
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
192
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
193
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
194
|
+
: [];
|
|
195
|
+
const attributes = Object.fromEntries(
|
|
196
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
197
|
+
);
|
|
190
198
|
|
|
191
199
|
for (const tableConfig of tableConfigs) {
|
|
192
200
|
const tableName = tableConfig.table;
|
|
@@ -221,7 +221,15 @@ class MySQLReplicator extends BaseReplicator {
|
|
|
221
221
|
continue;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
const
|
|
224
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
225
|
+
|
|
226
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
227
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
228
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
229
|
+
: [];
|
|
230
|
+
const attributes = Object.fromEntries(
|
|
231
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
232
|
+
);
|
|
225
233
|
|
|
226
234
|
for (const tableConfig of tableConfigs) {
|
|
227
235
|
const tableName = tableConfig.table;
|
|
@@ -183,7 +183,15 @@ class PlanetScaleReplicator extends BaseReplicator {
|
|
|
183
183
|
continue;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
const
|
|
186
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
187
|
+
|
|
188
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
189
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
190
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
191
|
+
: [];
|
|
192
|
+
const attributes = Object.fromEntries(
|
|
193
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
194
|
+
);
|
|
187
195
|
|
|
188
196
|
for (const tableConfig of tableConfigs) {
|
|
189
197
|
const tableName = tableConfig.table;
|
|
@@ -225,7 +225,15 @@ class PostgresReplicator extends BaseReplicator {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// Get resource attributes from current version
|
|
228
|
-
const
|
|
228
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
229
|
+
|
|
230
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
231
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
232
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
233
|
+
: [];
|
|
234
|
+
const attributes = Object.fromEntries(
|
|
235
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
236
|
+
);
|
|
229
237
|
|
|
230
238
|
// Sync each table configured for this resource
|
|
231
239
|
for (const tableConfig of tableConfigs) {
|
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
|
|
8
8
|
import tryFn from "#src/concerns/try-fn.js";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Filter out plugin attributes from attributes object
|
|
12
|
+
* Plugin attributes are internal implementation details and should not be replicated
|
|
13
|
+
* @param {Object} attributes - All attributes including plugin attributes
|
|
14
|
+
* @param {Object} resource - Resource instance with schema._pluginAttributes
|
|
15
|
+
* @returns {Object} Filtered attributes (user attributes only)
|
|
16
|
+
*/
|
|
17
|
+
function filterPluginAttributes(attributes, resource) {
|
|
18
|
+
if (!resource?.schema?._pluginAttributes) {
|
|
19
|
+
return attributes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pluginAttrNames = Object.values(resource.schema._pluginAttributes).flat();
|
|
23
|
+
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
Object.entries(attributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
/**
|
|
11
30
|
* Parse s3db field type notation (e.g., 'string|required|maxlength:50')
|
|
12
31
|
*/
|
|
@@ -177,7 +177,15 @@ class TursoReplicator extends BaseReplicator {
|
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
const
|
|
180
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
181
|
+
|
|
182
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
183
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
184
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
185
|
+
: [];
|
|
186
|
+
const attributes = Object.fromEntries(
|
|
187
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
188
|
+
);
|
|
181
189
|
|
|
182
190
|
for (const tableConfig of tableConfigs) {
|
|
183
191
|
const tableName = tableConfig.table;
|
|
@@ -183,13 +183,13 @@ export class VectorPlugin extends Plugin {
|
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
// Add tracking field to schema if not present
|
|
186
|
+
// Add tracking field to schema if not present using plugin API
|
|
187
187
|
if (!resource.schema.attributes[trackingFieldName]) {
|
|
188
|
-
resource.
|
|
188
|
+
resource.addPluginAttribute(trackingFieldName, {
|
|
189
189
|
type: 'boolean',
|
|
190
190
|
optional: true,
|
|
191
191
|
default: false
|
|
192
|
-
};
|
|
192
|
+
}, 'VectorPlugin');
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// Emit event
|
package/src/resource.class.js
CHANGED
|
@@ -7,6 +7,7 @@ import { PromisePool } from "@supercharge/promise-pool";
|
|
|
7
7
|
import { chunk, cloneDeep, merge, isEmpty, isObject } from "lodash-es";
|
|
8
8
|
|
|
9
9
|
import Schema from "./schema.class.js";
|
|
10
|
+
import { ValidatorManager } from "./validator.class.js";
|
|
10
11
|
import { streamToString } from "./stream/index.js";
|
|
11
12
|
import tryFn, { tryFnSync } from "./concerns/try-fn.js";
|
|
12
13
|
import { ResourceReader, ResourceWriter } from "./stream/index.js"
|
|
@@ -425,6 +426,164 @@ export class Resource extends AsyncEventEmitter {
|
|
|
425
426
|
return { oldAttributes, newAttributes };
|
|
426
427
|
}
|
|
427
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Add a plugin-created attribute to the resource schema
|
|
431
|
+
* This ensures plugin attributes don't interfere with user-defined attributes
|
|
432
|
+
* by using a separate mapping namespace (p0, p1, p2, ...)
|
|
433
|
+
*
|
|
434
|
+
* @param {string} name - Attribute name (e.g., '_hasEmbedding', 'clusterId')
|
|
435
|
+
* @param {Object|string} definition - Attribute definition
|
|
436
|
+
* @param {string} pluginName - Name of plugin adding the attribute
|
|
437
|
+
* @returns {void}
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* // VectorPlugin adding tracking field
|
|
441
|
+
* resource.addPluginAttribute('_hasEmbedding', {
|
|
442
|
+
* type: 'boolean',
|
|
443
|
+
* optional: true,
|
|
444
|
+
* default: false
|
|
445
|
+
* }, 'VectorPlugin');
|
|
446
|
+
*
|
|
447
|
+
* // Shorthand notation
|
|
448
|
+
* resource.addPluginAttribute('clusterId', 'string|optional', 'VectorPlugin');
|
|
449
|
+
*/
|
|
450
|
+
addPluginAttribute(name, definition, pluginName) {
|
|
451
|
+
if (!pluginName) {
|
|
452
|
+
throw new ResourceError(
|
|
453
|
+
'Plugin name is required when adding plugin attributes',
|
|
454
|
+
{ resource: this.name, attribute: name }
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// If attribute already exists and is not a plugin attribute, throw error
|
|
459
|
+
const existingDef = this.schema.getAttributeDefinition(name);
|
|
460
|
+
if (existingDef && (!existingDef.__plugin__ || existingDef.__plugin__ !== pluginName)) {
|
|
461
|
+
throw new ResourceError(
|
|
462
|
+
`Attribute '${name}' already exists and is not from plugin '${pluginName}'`,
|
|
463
|
+
{ resource: this.name, attribute: name, plugin: pluginName }
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Use the definition as-is (string or object)
|
|
468
|
+
// The schema preprocessor will handle string notation validation
|
|
469
|
+
let defObject = definition;
|
|
470
|
+
if (typeof definition === 'object' && definition !== null) {
|
|
471
|
+
// Clone to avoid mutation
|
|
472
|
+
defObject = { ...definition };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Mark as plugin-created with metadata
|
|
476
|
+
// For string definitions, we need to preserve them but track plugin ownership separately
|
|
477
|
+
if (typeof defObject === 'object' && defObject !== null) {
|
|
478
|
+
defObject.__plugin__ = pluginName;
|
|
479
|
+
defObject.__pluginCreated__ = Date.now();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Add to schema attributes
|
|
483
|
+
// Store original definition (string or object) as the validator expects
|
|
484
|
+
this.schema.attributes[name] = defObject;
|
|
485
|
+
|
|
486
|
+
// Also update resource.attributes to keep them in sync
|
|
487
|
+
this.attributes[name] = defObject;
|
|
488
|
+
|
|
489
|
+
// For string definitions, add metadata separately
|
|
490
|
+
if (typeof defObject === 'string') {
|
|
491
|
+
// Create a marker object to track plugin ownership in a parallel structure
|
|
492
|
+
if (!this.schema._pluginAttributeMetadata) {
|
|
493
|
+
this.schema._pluginAttributeMetadata = {};
|
|
494
|
+
}
|
|
495
|
+
this.schema._pluginAttributeMetadata[name] = {
|
|
496
|
+
__plugin__: pluginName,
|
|
497
|
+
__pluginCreated__: Date.now()
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Regenerate plugin mapping only (not user mapping)
|
|
502
|
+
this.schema.regeneratePluginMapping();
|
|
503
|
+
|
|
504
|
+
// Regenerate hooks for the new attribute
|
|
505
|
+
if (this.schema.options.generateAutoHooks) {
|
|
506
|
+
this.schema.generateAutoHooks();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Recompile validator to include new attribute
|
|
510
|
+
const processedAttributes = this.schema.preprocessAttributesForValidation(this.schema.attributes);
|
|
511
|
+
this.schema.validator = new ValidatorManager({ autoEncrypt: false }).compile(merge(
|
|
512
|
+
{ $$async: true, $$strict: false },
|
|
513
|
+
processedAttributes
|
|
514
|
+
));
|
|
515
|
+
|
|
516
|
+
// Emit event
|
|
517
|
+
if (this.database) {
|
|
518
|
+
this.database.emit('plugin-attribute-added', {
|
|
519
|
+
resource: this.name,
|
|
520
|
+
attribute: name,
|
|
521
|
+
plugin: pluginName,
|
|
522
|
+
definition: defObject
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Remove a plugin-created attribute from the resource schema
|
|
529
|
+
* Called when a plugin is uninstalled or no longer needs the attribute
|
|
530
|
+
*
|
|
531
|
+
* @param {string} name - Attribute name to remove
|
|
532
|
+
* @param {string} [pluginName] - Optional plugin name for safety check
|
|
533
|
+
* @returns {boolean} True if attribute was removed, false if not found
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* resource.removePluginAttribute('_hasEmbedding', 'VectorPlugin');
|
|
537
|
+
*/
|
|
538
|
+
removePluginAttribute(name, pluginName = null) {
|
|
539
|
+
const attrDef = this.schema.getAttributeDefinition(name);
|
|
540
|
+
|
|
541
|
+
// Check metadata for string definitions
|
|
542
|
+
const metadata = this.schema._pluginAttributeMetadata?.[name];
|
|
543
|
+
const isPluginAttr = (typeof attrDef === 'object' && attrDef?.__plugin__) || metadata;
|
|
544
|
+
|
|
545
|
+
// Check if attribute exists and is a plugin attribute
|
|
546
|
+
if (!attrDef || !isPluginAttr) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Get plugin name from either object or metadata
|
|
551
|
+
const actualPlugin = attrDef?.__plugin__ || metadata?.__plugin__;
|
|
552
|
+
|
|
553
|
+
// Safety check: if pluginName provided, ensure it matches
|
|
554
|
+
if (pluginName && actualPlugin !== pluginName) {
|
|
555
|
+
throw new ResourceError(
|
|
556
|
+
`Attribute '${name}' belongs to plugin '${actualPlugin}', not '${pluginName}'`,
|
|
557
|
+
{ resource: this.name, attribute: name, actualPlugin, requestedPlugin: pluginName }
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Remove from schema
|
|
562
|
+
delete this.schema.attributes[name];
|
|
563
|
+
|
|
564
|
+
// Also remove from resource.attributes to keep them in sync
|
|
565
|
+
delete this.attributes[name];
|
|
566
|
+
|
|
567
|
+
// Remove metadata if it exists
|
|
568
|
+
if (this.schema._pluginAttributeMetadata?.[name]) {
|
|
569
|
+
delete this.schema._pluginAttributeMetadata[name];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Regenerate plugin mapping
|
|
573
|
+
this.schema.regeneratePluginMapping();
|
|
574
|
+
|
|
575
|
+
// Emit event
|
|
576
|
+
if (this.database) {
|
|
577
|
+
this.database.emit('plugin-attribute-removed', {
|
|
578
|
+
resource: this.name,
|
|
579
|
+
attribute: name,
|
|
580
|
+
plugin: actualPlugin
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
|
|
428
587
|
/**
|
|
429
588
|
* Add a hook function for a specific event
|
|
430
589
|
* @param {string} event - Hook event (beforeInsert, afterInsert, etc.)
|
|
@@ -3284,11 +3443,17 @@ export class Resource extends AsyncEventEmitter {
|
|
|
3284
3443
|
unmappedMetadata = ok ? unmapped : metadata;
|
|
3285
3444
|
// Helper function to filter out internal S3DB fields
|
|
3286
3445
|
// Preserve geo-related fields (_geohash, _geohash_zoom*) for GeoPlugin
|
|
3446
|
+
// Preserve plugin attributes (fields in _pluginAttributes)
|
|
3287
3447
|
const filterInternalFields = (obj) => {
|
|
3288
3448
|
if (!obj || typeof obj !== 'object') return obj;
|
|
3289
3449
|
const filtered = {};
|
|
3450
|
+
const pluginAttrNames = this.schema._pluginAttributes
|
|
3451
|
+
? Object.values(this.schema._pluginAttributes).flat()
|
|
3452
|
+
: [];
|
|
3453
|
+
|
|
3290
3454
|
for (const [key, value] of Object.entries(obj)) {
|
|
3291
|
-
if
|
|
3455
|
+
// Keep field if it doesn't start with _, or if it's a special field, or if it's a plugin attribute
|
|
3456
|
+
if (!key.startsWith('_') || key === '_geohash' || key.startsWith('_geohash_zoom') || pluginAttrNames.includes(key)) {
|
|
3292
3457
|
filtered[key] = value;
|
|
3293
3458
|
}
|
|
3294
3459
|
}
|
|
@@ -3315,7 +3480,19 @@ export class Resource extends AsyncEventEmitter {
|
|
|
3315
3480
|
if (hasOverflow && body) {
|
|
3316
3481
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(JSON.parse(body)));
|
|
3317
3482
|
if (okBody) {
|
|
3318
|
-
|
|
3483
|
+
// Extract pluginMap for backwards compatibility when plugins are added/removed
|
|
3484
|
+
let pluginMapFromMeta = null;
|
|
3485
|
+
// S3 metadata keys are case-insensitive and stored as lowercase
|
|
3486
|
+
if (metadata && metadata._pluginmap) {
|
|
3487
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(() =>
|
|
3488
|
+
Promise.resolve(typeof metadata._pluginmap === 'string' ? JSON.parse(metadata._pluginmap) : metadata._pluginmap)
|
|
3489
|
+
);
|
|
3490
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(() =>
|
|
3494
|
+
this.schema.unmapper(parsedBody, undefined, pluginMapFromMeta)
|
|
3495
|
+
);
|
|
3319
3496
|
bodyData = okUnmap ? unmappedBody : {};
|
|
3320
3497
|
}
|
|
3321
3498
|
}
|
|
@@ -3330,11 +3507,21 @@ export class Resource extends AsyncEventEmitter {
|
|
|
3330
3507
|
if (behavior === 'body-only') {
|
|
3331
3508
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(body ? JSON.parse(body) : {}));
|
|
3332
3509
|
let mapFromMeta = this.schema.map;
|
|
3510
|
+
let pluginMapFromMeta = null;
|
|
3511
|
+
|
|
3333
3512
|
if (metadata && metadata._map) {
|
|
3334
3513
|
const [okMap, errMap, parsedMap] = await tryFn(() => Promise.resolve(typeof metadata._map === 'string' ? JSON.parse(metadata._map) : metadata._map));
|
|
3335
3514
|
mapFromMeta = okMap ? parsedMap : this.schema.map;
|
|
3336
3515
|
}
|
|
3337
|
-
|
|
3516
|
+
|
|
3517
|
+
// S3 metadata keys are case-insensitive and stored as lowercase
|
|
3518
|
+
// So _pluginMap becomes _pluginmap
|
|
3519
|
+
if (metadata && metadata._pluginmap) {
|
|
3520
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(() => Promise.resolve(typeof metadata._pluginmap === 'string' ? JSON.parse(metadata._pluginmap) : metadata._pluginmap));
|
|
3521
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(() => this.schema.unmapper(parsedBody, mapFromMeta, pluginMapFromMeta));
|
|
3338
3525
|
const result = okUnmap ? { ...unmappedBody, id } : { id };
|
|
3339
3526
|
Object.keys(result).forEach(k => { result[k] = fixValue(result[k]); });
|
|
3340
3527
|
return result;
|
|
@@ -3344,7 +3531,19 @@ export class Resource extends AsyncEventEmitter {
|
|
|
3344
3531
|
if (behavior === 'user-managed' && body && body.trim() !== '') {
|
|
3345
3532
|
const [okBody, errBody, parsedBody] = await tryFn(() => Promise.resolve(JSON.parse(body)));
|
|
3346
3533
|
if (okBody) {
|
|
3347
|
-
|
|
3534
|
+
// Extract pluginMap for backwards compatibility when plugins are added/removed
|
|
3535
|
+
let pluginMapFromMeta = null;
|
|
3536
|
+
// S3 metadata keys are case-insensitive and stored as lowercase
|
|
3537
|
+
if (metadata && metadata._pluginmap) {
|
|
3538
|
+
const [okPluginMap, errPluginMap, parsedPluginMap] = await tryFn(() =>
|
|
3539
|
+
Promise.resolve(typeof metadata._pluginmap === 'string' ? JSON.parse(metadata._pluginmap) : metadata._pluginmap)
|
|
3540
|
+
);
|
|
3541
|
+
pluginMapFromMeta = okPluginMap ? parsedPluginMap : null;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
const [okUnmap, errUnmap, unmappedBody] = await tryFn(() =>
|
|
3545
|
+
this.schema.unmapper(parsedBody, undefined, pluginMapFromMeta)
|
|
3546
|
+
);
|
|
3348
3547
|
const bodyData = okUnmap ? unmappedBody : {};
|
|
3349
3548
|
const merged = { ...bodyData, ...unmappedMetadata, id };
|
|
3350
3549
|
Object.keys(merged).forEach(k => { merged[k] = fixValue(merged[k]); });
|