s3db.js 11.0.4 → 11.1.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 +59 -2
- package/dist/s3db.cjs.js +502 -84
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +502 -84
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -1
- package/src/concerns/crypto.js +7 -14
- package/src/plugins/eventual-consistency/analytics.js +44 -22
- package/src/plugins/eventual-consistency/consolidation.js +228 -80
- package/src/plugins/eventual-consistency/helpers.js +24 -8
- package/src/plugins/eventual-consistency/index.js +73 -1
- package/src/plugins/eventual-consistency/install.js +1 -0
- package/src/plugins/eventual-consistency/utils.js +154 -4
- package/src/concerns/advanced-metadata-encoding.js +0 -440
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
runConsolidation
|
|
18
18
|
} from "./consolidation.js";
|
|
19
19
|
import { runGarbageCollection } from "./garbage-collection.js";
|
|
20
|
-
import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getYearByWeek, getMonthByWeek, getMonthByHour, getTopRecords } from "./analytics.js";
|
|
20
|
+
import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getYearByWeek, getMonthByWeek, getMonthByHour, getTopRecords, getYearByDay, getWeekByDay, getWeekByHour, getLastNHours, getLastNWeeks, getLastNMonths } from "./analytics.js";
|
|
21
21
|
import { onInstall, onStart, onStop, watchForResource, completeFieldSetup } from "./install.js";
|
|
22
22
|
|
|
23
23
|
export class EventualConsistencyPlugin extends Plugin {
|
|
@@ -459,6 +459,78 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
459
459
|
async getTopRecords(resourceName, field, options = {}) {
|
|
460
460
|
return await getTopRecords(resourceName, field, options, this.fieldHandlers);
|
|
461
461
|
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get analytics for entire year, broken down by days
|
|
465
|
+
* @param {string} resourceName - Resource name
|
|
466
|
+
* @param {string} field - Field name
|
|
467
|
+
* @param {number} year - Year (e.g., 2025)
|
|
468
|
+
* @param {Object} options - Options
|
|
469
|
+
* @returns {Promise<Array>} Daily analytics for the year (up to 365/366 records)
|
|
470
|
+
*/
|
|
471
|
+
async getYearByDay(resourceName, field, year, options = {}) {
|
|
472
|
+
return await getYearByDay(resourceName, field, year, options, this.fieldHandlers);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Get analytics for entire week, broken down by days
|
|
477
|
+
* @param {string} resourceName - Resource name
|
|
478
|
+
* @param {string} field - Field name
|
|
479
|
+
* @param {string} week - Week in YYYY-Www format (e.g., '2025-W42')
|
|
480
|
+
* @param {Object} options - Options
|
|
481
|
+
* @returns {Promise<Array>} Daily analytics for the week (7 records)
|
|
482
|
+
*/
|
|
483
|
+
async getWeekByDay(resourceName, field, week, options = {}) {
|
|
484
|
+
return await getWeekByDay(resourceName, field, week, options, this.fieldHandlers);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get analytics for entire week, broken down by hours
|
|
489
|
+
* @param {string} resourceName - Resource name
|
|
490
|
+
* @param {string} field - Field name
|
|
491
|
+
* @param {string} week - Week in YYYY-Www format (e.g., '2025-W42')
|
|
492
|
+
* @param {Object} options - Options
|
|
493
|
+
* @returns {Promise<Array>} Hourly analytics for the week (168 records)
|
|
494
|
+
*/
|
|
495
|
+
async getWeekByHour(resourceName, field, week, options = {}) {
|
|
496
|
+
return await getWeekByHour(resourceName, field, week, options, this.fieldHandlers);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get analytics for last N hours
|
|
501
|
+
* @param {string} resourceName - Resource name
|
|
502
|
+
* @param {string} field - Field name
|
|
503
|
+
* @param {number} hours - Number of hours to look back (default: 24)
|
|
504
|
+
* @param {Object} options - Options
|
|
505
|
+
* @returns {Promise<Array>} Hourly analytics
|
|
506
|
+
*/
|
|
507
|
+
async getLastNHours(resourceName, field, hours = 24, options = {}) {
|
|
508
|
+
return await getLastNHours(resourceName, field, hours, options, this.fieldHandlers);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get analytics for last N weeks
|
|
513
|
+
* @param {string} resourceName - Resource name
|
|
514
|
+
* @param {string} field - Field name
|
|
515
|
+
* @param {number} weeks - Number of weeks to look back (default: 4)
|
|
516
|
+
* @param {Object} options - Options
|
|
517
|
+
* @returns {Promise<Array>} Weekly analytics
|
|
518
|
+
*/
|
|
519
|
+
async getLastNWeeks(resourceName, field, weeks = 4, options = {}) {
|
|
520
|
+
return await getLastNWeeks(resourceName, field, weeks, options, this.fieldHandlers);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get analytics for last N months
|
|
525
|
+
* @param {string} resourceName - Resource name
|
|
526
|
+
* @param {string} field - Field name
|
|
527
|
+
* @param {number} months - Number of months to look back (default: 12)
|
|
528
|
+
* @param {Object} options - Options
|
|
529
|
+
* @returns {Promise<Array>} Monthly analytics
|
|
530
|
+
*/
|
|
531
|
+
async getLastNMonths(resourceName, field, months = 12, options = {}) {
|
|
532
|
+
return await getLastNMonths(resourceName, field, months, options, this.fieldHandlers);
|
|
533
|
+
}
|
|
462
534
|
}
|
|
463
535
|
|
|
464
536
|
export default EventualConsistencyPlugin;
|
|
@@ -95,6 +95,7 @@ export async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
95
95
|
id: 'string|required',
|
|
96
96
|
originalId: 'string|required',
|
|
97
97
|
field: 'string|required',
|
|
98
|
+
fieldPath: 'string|optional', // Support for nested field paths (e.g., 'utmResults.medium')
|
|
98
99
|
value: 'number|required',
|
|
99
100
|
operation: 'string|required',
|
|
100
101
|
timestamp: 'string|required',
|
|
@@ -160,19 +160,169 @@ export function createFieldHandler(resourceName, fieldName) {
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Validate nested path in resource schema
|
|
165
|
+
* Allows 1 level of nesting after 'json' type fields
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} resource - Resource object
|
|
168
|
+
* @param {string} fieldPath - Dot-notation path (e.g., 'utmResults.medium.google')
|
|
169
|
+
* @returns {Object} { valid: boolean, rootField: string, fullPath: string, error?: string }
|
|
170
|
+
*/
|
|
171
|
+
export function validateNestedPath(resource, fieldPath) {
|
|
172
|
+
const parts = fieldPath.split('.');
|
|
173
|
+
const rootField = parts[0];
|
|
174
|
+
|
|
175
|
+
// Root field must exist in resource attributes
|
|
176
|
+
if (!resource.attributes || !resource.attributes[rootField]) {
|
|
177
|
+
return {
|
|
178
|
+
valid: false,
|
|
179
|
+
rootField,
|
|
180
|
+
fullPath: fieldPath,
|
|
181
|
+
error: `Root field "${rootField}" not found in resource attributes`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// If no nesting, just return valid
|
|
186
|
+
if (parts.length === 1) {
|
|
187
|
+
return { valid: true, rootField, fullPath: fieldPath };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Validate nested path
|
|
191
|
+
let current = resource.attributes[rootField];
|
|
192
|
+
let foundJson = false;
|
|
193
|
+
let levelsAfterJson = 0;
|
|
194
|
+
|
|
195
|
+
for (let i = 1; i < parts.length; i++) {
|
|
196
|
+
const part = parts[i];
|
|
197
|
+
|
|
198
|
+
// If we found 'json' before, count levels
|
|
199
|
+
if (foundJson) {
|
|
200
|
+
levelsAfterJson++;
|
|
201
|
+
// Only allow 1 level after 'json'
|
|
202
|
+
if (levelsAfterJson > 1) {
|
|
203
|
+
return {
|
|
204
|
+
valid: false,
|
|
205
|
+
rootField,
|
|
206
|
+
fullPath: fieldPath,
|
|
207
|
+
error: `Path "${fieldPath}" exceeds 1 level after 'json' field. Maximum nesting after 'json' is 1 level.`
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// After 'json', we can't validate further, but we allow 1 level
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if current level is 'json' type
|
|
215
|
+
if (typeof current === 'string') {
|
|
216
|
+
if (current === 'json' || current.startsWith('json|')) {
|
|
217
|
+
foundJson = true;
|
|
218
|
+
levelsAfterJson++;
|
|
219
|
+
// Allow 1 level after json
|
|
220
|
+
if (levelsAfterJson > 1) {
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
rootField,
|
|
224
|
+
fullPath: fieldPath,
|
|
225
|
+
error: `Path "${fieldPath}" exceeds 1 level after 'json' field`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// Other string types can't be nested
|
|
231
|
+
return {
|
|
232
|
+
valid: false,
|
|
233
|
+
rootField,
|
|
234
|
+
fullPath: fieldPath,
|
|
235
|
+
error: `Field "${parts.slice(0, i).join('.')}" is type "${current}" and cannot be nested`
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check if current is an object with nested structure
|
|
240
|
+
if (typeof current === 'object') {
|
|
241
|
+
// Check for $$type
|
|
242
|
+
if (current.$$type) {
|
|
243
|
+
const type = current.$$type;
|
|
244
|
+
if (type === 'json' || type.includes('json')) {
|
|
245
|
+
foundJson = true;
|
|
246
|
+
levelsAfterJson++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (type !== 'object' && !type.includes('object')) {
|
|
250
|
+
return {
|
|
251
|
+
valid: false,
|
|
252
|
+
rootField,
|
|
253
|
+
fullPath: fieldPath,
|
|
254
|
+
error: `Field "${parts.slice(0, i).join('.')}" is type "${type}" and cannot be nested`
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Navigate to next level
|
|
260
|
+
if (!current[part]) {
|
|
261
|
+
return {
|
|
262
|
+
valid: false,
|
|
263
|
+
rootField,
|
|
264
|
+
fullPath: fieldPath,
|
|
265
|
+
error: `Field "${part}" not found in "${parts.slice(0, i).join('.')}"`
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
current = current[part];
|
|
269
|
+
} else {
|
|
270
|
+
return {
|
|
271
|
+
valid: false,
|
|
272
|
+
rootField,
|
|
273
|
+
fullPath: fieldPath,
|
|
274
|
+
error: `Invalid structure at "${parts.slice(0, i).join('.')}"`
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { valid: true, rootField, fullPath: fieldPath };
|
|
280
|
+
}
|
|
281
|
+
|
|
163
282
|
/**
|
|
164
283
|
* Resolve field and plugin from arguments
|
|
284
|
+
* Supports dot notation for nested fields (e.g., 'utmResults.medium.google')
|
|
285
|
+
*
|
|
165
286
|
* @param {Object} resource - Resource object
|
|
166
|
-
* @param {string} field - Field name
|
|
287
|
+
* @param {string} field - Field name or path (supports dot notation)
|
|
167
288
|
* @param {*} value - Value (for error reporting)
|
|
168
|
-
* @returns {Object} Resolved field and plugin handler
|
|
169
|
-
* @throws {Error} If field or plugin not found
|
|
289
|
+
* @returns {Object} Resolved field, path, and plugin handler
|
|
290
|
+
* @throws {Error} If field or plugin not found, or path is invalid
|
|
170
291
|
*/
|
|
171
292
|
export function resolveFieldAndPlugin(resource, field, value) {
|
|
172
293
|
if (!resource._eventualConsistencyPlugins) {
|
|
173
294
|
throw new Error(`No eventual consistency plugins configured for this resource`);
|
|
174
295
|
}
|
|
175
296
|
|
|
297
|
+
// Check if field contains dot notation (nested path)
|
|
298
|
+
if (field.includes('.')) {
|
|
299
|
+
const validation = validateNestedPath(resource, field);
|
|
300
|
+
|
|
301
|
+
if (!validation.valid) {
|
|
302
|
+
throw new Error(validation.error);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Get plugin for root field
|
|
306
|
+
const rootField = validation.rootField;
|
|
307
|
+
const fieldPlugin = resource._eventualConsistencyPlugins[rootField];
|
|
308
|
+
|
|
309
|
+
if (!fieldPlugin) {
|
|
310
|
+
const availableFields = Object.keys(resource._eventualConsistencyPlugins).join(', ');
|
|
311
|
+
throw new Error(
|
|
312
|
+
`No eventual consistency plugin found for root field "${rootField}". ` +
|
|
313
|
+
`Available fields: ${availableFields}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
field: rootField, // Root field for plugin lookup
|
|
319
|
+
fieldPath: field, // Full path for nested access
|
|
320
|
+
value,
|
|
321
|
+
plugin: fieldPlugin
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Simple field (no nesting)
|
|
176
326
|
const fieldPlugin = resource._eventualConsistencyPlugins[field];
|
|
177
327
|
|
|
178
328
|
if (!fieldPlugin) {
|
|
@@ -183,7 +333,7 @@ export function resolveFieldAndPlugin(resource, field, value) {
|
|
|
183
333
|
);
|
|
184
334
|
}
|
|
185
335
|
|
|
186
|
-
return { field, value, plugin: fieldPlugin };
|
|
336
|
+
return { field, fieldPath: field, value, plugin: fieldPlugin };
|
|
187
337
|
}
|
|
188
338
|
|
|
189
339
|
/**
|