@objectstack/driver-memory 4.0.4 → 4.0.5

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.
@@ -1,260 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { ObjectStackManifest } from '@objectstack/spec/system';
4
-
5
- /**
6
- * In-Memory Driver Plugin Manifest
7
- *
8
- * Reference implementation of a storage driver that stores data in memory.
9
- * Demonstrates the driver protocol implementation pattern.
10
- */
11
- const MemoryDriverPlugin: ObjectStackManifest = {
12
- id: 'com.objectstack.driver.memory',
13
- name: 'In-Memory Driver',
14
- version: '1.0.0',
15
- type: 'driver',
16
- description: 'A reference specification implementation of the DriverInterface using in-memory arrays. Suitable for testing and development.',
17
-
18
- configuration: {
19
- title: 'Memory Driver Settings',
20
- properties: {
21
- seedData: {
22
- type: 'boolean',
23
- default: false,
24
- description: 'Pre-populate the database with example data on startup'
25
- }
26
- }
27
- },
28
-
29
- // Plugin Capability Declaration
30
- capabilities: {
31
- // Protocols This Driver Implements
32
- implements: [
33
- {
34
- protocol: {
35
- id: 'com.objectstack.protocol.storage.v1',
36
- label: 'Storage Protocol v1',
37
- version: { major: 1, minor: 0, patch: 0 },
38
- description: 'Standard data storage and retrieval operations',
39
- },
40
- conformance: 'partial',
41
- implementedFeatures: [
42
- 'basic_crud',
43
- 'pagination',
44
- ],
45
- features: [
46
- {
47
- name: 'basic_crud',
48
- enabled: true,
49
- description: 'Create, read, update, delete operations',
50
- },
51
- {
52
- name: 'pagination',
53
- enabled: true,
54
- description: 'Basic pagination via limit/offset',
55
- },
56
- {
57
- name: 'query_filters',
58
- enabled: false,
59
- description: 'Advanced query filtering',
60
- },
61
- {
62
- name: 'aggregations',
63
- enabled: false,
64
- description: 'Count, sum, avg operations',
65
- },
66
- {
67
- name: 'sorting',
68
- enabled: false,
69
- description: 'Result set sorting',
70
- },
71
- {
72
- name: 'transactions',
73
- enabled: false,
74
- description: 'ACID transaction support',
75
- },
76
- {
77
- name: 'joins',
78
- enabled: false,
79
- description: 'Cross-object joins',
80
- },
81
- ],
82
- certified: false,
83
- },
84
- ],
85
-
86
- // Interfaces This Driver Provides
87
- provides: [
88
- {
89
- id: 'com.objectstack.driver.memory.interface.driver',
90
- name: 'DriverInterface',
91
- description: 'Standard ObjectStack driver interface for data operations',
92
- version: { major: 1, minor: 0, patch: 0 },
93
- stability: 'stable',
94
- methods: [
95
- {
96
- name: 'connect',
97
- description: 'Initialize driver connection',
98
- parameters: [],
99
- returnType: 'Promise<void>',
100
- async: true,
101
- },
102
- {
103
- name: 'disconnect',
104
- description: 'Close driver connection',
105
- parameters: [],
106
- returnType: 'Promise<void>',
107
- async: true,
108
- },
109
- {
110
- name: 'create',
111
- description: 'Create a new record',
112
- parameters: [
113
- {
114
- name: 'object',
115
- type: 'string',
116
- required: true,
117
- description: 'Object name',
118
- },
119
- {
120
- name: 'data',
121
- type: 'Record<string, any>',
122
- required: true,
123
- description: 'Record data',
124
- },
125
- ],
126
- returnType: 'Promise<any>',
127
- async: true,
128
- },
129
- {
130
- name: 'find',
131
- description: 'Query records',
132
- parameters: [
133
- {
134
- name: 'object',
135
- type: 'string',
136
- required: true,
137
- description: 'Object name',
138
- },
139
- {
140
- name: 'query',
141
- type: 'QueryInput',
142
- required: false,
143
- description: 'Query parameters',
144
- },
145
- ],
146
- returnType: 'Promise<any[]>',
147
- async: true,
148
- },
149
- {
150
- name: 'findOne',
151
- description: 'Find a single record by ID',
152
- parameters: [
153
- {
154
- name: 'object',
155
- type: 'string',
156
- required: true,
157
- description: 'Object name',
158
- },
159
- {
160
- name: 'id',
161
- type: 'string',
162
- required: true,
163
- description: 'Record ID',
164
- },
165
- ],
166
- returnType: 'Promise<any>',
167
- async: true,
168
- },
169
- {
170
- name: 'update',
171
- description: 'Update a record',
172
- parameters: [
173
- {
174
- name: 'object',
175
- type: 'string',
176
- required: true,
177
- description: 'Object name',
178
- },
179
- {
180
- name: 'id',
181
- type: 'string',
182
- required: true,
183
- description: 'Record ID',
184
- },
185
- {
186
- name: 'data',
187
- type: 'Record<string, any>',
188
- required: true,
189
- description: 'Updated record data',
190
- },
191
- ],
192
- returnType: 'Promise<any>',
193
- async: true,
194
- },
195
- {
196
- name: 'delete',
197
- description: 'Delete a record',
198
- parameters: [
199
- {
200
- name: 'object',
201
- type: 'string',
202
- required: true,
203
- description: 'Object name',
204
- },
205
- {
206
- name: 'id',
207
- type: 'string',
208
- required: true,
209
- description: 'Record ID',
210
- },
211
- ],
212
- returnType: 'Promise<boolean>',
213
- async: true,
214
- },
215
- {
216
- name: 'count',
217
- description: 'Count records',
218
- parameters: [
219
- {
220
- name: 'object',
221
- type: 'string',
222
- required: true,
223
- description: 'Object name',
224
- },
225
- {
226
- name: 'query',
227
- type: 'QueryInput',
228
- required: false,
229
- description: 'Query parameters',
230
- },
231
- ],
232
- returnType: 'Promise<number>',
233
- async: true,
234
- },
235
- ],
236
- },
237
- ],
238
-
239
- // No external plugin dependencies (this is a core driver)
240
- requires: [],
241
-
242
- // No extension points defined
243
- extensionPoints: [],
244
-
245
- // No extensions contributed
246
- extensions: [],
247
- },
248
-
249
- contributes: {
250
- drivers: [
251
- {
252
- id: 'memory',
253
- label: 'In-Memory Storage',
254
- description: 'Stores data in memory (volatile, for testing/development)',
255
- },
256
- ],
257
- }
258
- };
259
-
260
- export default MemoryDriverPlugin;
@@ -1,47 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { AnalyticsQuery, AnalyticsResult, AnalyticsStrategy, StrategyContext } from '@objectstack/spec/contracts';
4
-
5
- /**
6
- * InMemoryStrategy — Priority 3
7
- *
8
- * Delegates to an existing `IAnalyticsService` instance that was registered
9
- * as a fallback (typically `MemoryAnalyticsService` from this package).
10
- *
11
- * This is the lowest-priority strategy, used in:
12
- * - `dev` / `test` environments
13
- * - Any runtime where the backing driver is in-memory
14
- */
15
- export class InMemoryStrategy implements AnalyticsStrategy {
16
- readonly name = 'InMemoryStrategy';
17
- readonly priority = 30;
18
-
19
- canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {
20
- if (!query.cube) return false;
21
- // Can handle when a fallback service exists
22
- if (ctx.fallbackService) return true;
23
- // Or when the driver is flagged as in-memory
24
- const caps = ctx.queryCapabilities(query.cube);
25
- return caps.inMemory;
26
- }
27
-
28
- async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {
29
- if (!ctx.fallbackService) {
30
- throw new Error(
31
- `[InMemoryStrategy] No fallback analytics service available for cube "${query.cube}". ` +
32
- 'Register a MemoryAnalyticsService or configure a driver with analytics support.'
33
- );
34
- }
35
- return ctx.fallbackService.query(query);
36
- }
37
-
38
- async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {
39
- if (ctx.fallbackService?.generateSql) {
40
- return ctx.fallbackService.generateSql(query);
41
- }
42
- return {
43
- sql: `-- InMemoryStrategy: SQL generation not supported for cube "${query.cube}"`,
44
- params: [],
45
- };
46
- }
47
- }
package/src/index.ts DELETED
@@ -1,32 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { InMemoryDriver } from './memory-driver.js';
4
-
5
- export { InMemoryDriver }; // Export class for direct usage
6
- export type { InMemoryDriverConfig, PersistenceAdapterInterface } from './memory-driver.js';
7
-
8
- export { FileSystemPersistenceAdapter } from './persistence/file-adapter.js';
9
- export { LocalStoragePersistenceAdapter } from './persistence/local-storage-adapter.js';
10
-
11
- export { MemoryAnalyticsService } from './memory-analytics.js';
12
- export type { MemoryAnalyticsConfig } from './memory-analytics.js';
13
-
14
- export { InMemoryStrategy } from './in-memory-strategy.js';
15
-
16
- export default {
17
- id: 'com.objectstack.driver.memory',
18
- version: '1.0.0',
19
-
20
- onEnable: async (context: any) => {
21
- const { logger, config, drivers } = context;
22
- logger.info('[Memory Driver] Initializing...');
23
-
24
- if (drivers) {
25
- const driver = new InMemoryDriver(config);
26
- drivers.register(driver);
27
- logger.info(`[Memory Driver] Registered driver: ${driver.name}`);
28
- } else {
29
- logger.warn('[Memory Driver] No driver registry found in context.');
30
- }
31
- }
32
- };
@@ -1,346 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, beforeEach } from 'vitest';
4
- import { InMemoryDriver } from './memory-driver.js';
5
- import { MemoryAnalyticsService } from './memory-analytics.js';
6
- import type { Cube } from '@objectstack/spec/data';
7
-
8
- describe('MemoryAnalyticsService', () => {
9
- let driver: InMemoryDriver;
10
- let service: MemoryAnalyticsService;
11
-
12
- beforeEach(async () => {
13
- // Initialize driver with sample data
14
- driver = new InMemoryDriver({
15
- initialData: {
16
- orders: [
17
- { id: 1, customer: 'Alice', status: 'completed', amount: 100, created_at: new Date('2024-01-15') },
18
- { id: 2, customer: 'Bob', status: 'completed', amount: 200, created_at: new Date('2024-01-16') },
19
- { id: 3, customer: 'Alice', status: 'pending', amount: 150, created_at: new Date('2024-01-17') },
20
- { id: 4, customer: 'Charlie', status: 'completed', amount: 300, created_at: new Date('2024-01-18') },
21
- { id: 5, customer: 'Bob', status: 'cancelled', amount: 50, created_at: new Date('2024-01-19') },
22
- ],
23
- products: [
24
- { id: 1, name: 'Laptop', category: 'electronics', price: 999, stock: 10 },
25
- { id: 2, name: 'Mouse', category: 'electronics', price: 25, stock: 100 },
26
- { id: 3, name: 'Desk', category: 'furniture', price: 299, stock: 5 },
27
- { id: 4, name: 'Chair', category: 'furniture', price: 199, stock: 8 },
28
- ]
29
- }
30
- });
31
-
32
- // Connect the driver to load initial data
33
- await driver.connect();
34
-
35
- // Define cubes
36
- const cubes: Cube[] = [
37
- {
38
- name: 'orders',
39
- title: 'Orders',
40
- sql: 'orders',
41
- measures: {
42
- count: {
43
- name: 'count',
44
- label: 'Order Count',
45
- type: 'count',
46
- sql: 'id'
47
- },
48
- totalAmount: {
49
- name: 'total_amount',
50
- label: 'Total Amount',
51
- type: 'sum',
52
- sql: 'amount'
53
- },
54
- avgAmount: {
55
- name: 'avg_amount',
56
- label: 'Average Amount',
57
- type: 'avg',
58
- sql: 'amount'
59
- }
60
- },
61
- dimensions: {
62
- customer: {
63
- name: 'customer',
64
- label: 'Customer',
65
- type: 'string',
66
- sql: 'customer'
67
- },
68
- status: {
69
- name: 'status',
70
- label: 'Status',
71
- type: 'string',
72
- sql: 'status'
73
- },
74
- createdAt: {
75
- name: 'created_at',
76
- label: 'Created At',
77
- type: 'time',
78
- sql: 'created_at',
79
- granularities: ['day', 'week', 'month']
80
- }
81
- },
82
- public: true
83
- },
84
- {
85
- name: 'products',
86
- title: 'Products',
87
- sql: 'products',
88
- measures: {
89
- count: {
90
- name: 'count',
91
- label: 'Product Count',
92
- type: 'count',
93
- sql: 'id'
94
- },
95
- avgPrice: {
96
- name: 'avg_price',
97
- label: 'Average Price',
98
- type: 'avg',
99
- sql: 'price'
100
- },
101
- totalStock: {
102
- name: 'total_stock',
103
- label: 'Total Stock',
104
- type: 'sum',
105
- sql: 'stock'
106
- }
107
- },
108
- dimensions: {
109
- category: {
110
- name: 'category',
111
- label: 'Category',
112
- type: 'string',
113
- sql: 'category'
114
- },
115
- name: {
116
- name: 'name',
117
- label: 'Product Name',
118
- type: 'string',
119
- sql: 'name'
120
- }
121
- },
122
- public: true
123
- }
124
- ];
125
-
126
- service = new MemoryAnalyticsService({ driver, cubes });
127
- });
128
-
129
- describe('getMeta', () => {
130
- it('should return metadata for all cubes', async () => {
131
- const meta = await service.getMeta();
132
-
133
- expect(meta).toHaveLength(2);
134
- expect(meta[0].name).toBe('orders');
135
- expect(meta[1].name).toBe('products');
136
- });
137
-
138
- it('should return metadata for a specific cube', async () => {
139
- const meta = await service.getMeta('orders');
140
-
141
- expect(meta).toHaveLength(1);
142
- expect(meta[0].name).toBe('orders');
143
- expect(meta[0].measures).toHaveLength(3);
144
- expect(meta[0].dimensions).toHaveLength(3);
145
- });
146
-
147
- it('should include measure and dimension details', async () => {
148
- const meta = await service.getMeta('orders');
149
- const cube = meta[0];
150
-
151
- const countMeasure = cube.measures.find(m => m.name === 'orders.count');
152
- expect(countMeasure).toBeDefined();
153
- expect(countMeasure?.type).toBe('count');
154
-
155
- const statusDim = cube.dimensions.find(d => d.name === 'orders.status');
156
- expect(statusDim).toBeDefined();
157
- expect(statusDim?.type).toBe('string');
158
- });
159
- });
160
-
161
- describe('query', () => {
162
- it('should execute a simple count query', async () => {
163
- const result = await service.query({
164
- cube: 'orders',
165
- measures: ['orders.count']
166
- });
167
-
168
- expect(result.rows).toHaveLength(1);
169
- expect(result.rows[0]['orders.count']).toBe(5);
170
- expect(result.fields).toHaveLength(1);
171
- expect(result.fields[0].name).toBe('orders.count');
172
- expect(result.fields[0].type).toBe('number');
173
- });
174
-
175
- it('should group by a dimension', async () => {
176
- const result = await service.query({
177
- cube: 'orders',
178
- measures: ['orders.count'],
179
- dimensions: ['orders.status']
180
- });
181
-
182
- expect(result.rows).toHaveLength(3); // completed, pending, cancelled
183
-
184
- const completedRow = result.rows.find(r => r['orders.status'] === 'completed');
185
- expect(completedRow).toBeDefined();
186
- expect(completedRow!['orders.count']).toBe(3);
187
- });
188
-
189
- it('should calculate sum aggregation', async () => {
190
- const result = await service.query({
191
- cube: 'orders',
192
- measures: ['orders.totalAmount'],
193
- dimensions: ['orders.customer']
194
- });
195
-
196
- const aliceRow = result.rows.find(r => r['orders.customer'] === 'Alice');
197
- expect(aliceRow).toBeDefined();
198
- expect(aliceRow!['orders.totalAmount']).toBe(250); // 100 + 150
199
- });
200
-
201
- it('should calculate average aggregation', async () => {
202
- const result = await service.query({
203
- cube: 'products',
204
- measures: ['products.avgPrice'],
205
- dimensions: ['products.category']
206
- });
207
-
208
- const electronicsRow = result.rows.find(r => r['products.category'] === 'electronics');
209
- expect(electronicsRow).toBeDefined();
210
- expect(electronicsRow!['products.avgPrice']).toBe(512); // (999 + 25) / 2
211
- });
212
-
213
- it('should support multiple measures', async () => {
214
- const result = await service.query({
215
- cube: 'orders',
216
- measures: ['orders.count', 'orders.totalAmount', 'orders.avgAmount']
217
- });
218
-
219
- expect(result.rows).toHaveLength(1);
220
- expect(result.rows[0]['orders.count']).toBe(5);
221
- expect(result.rows[0]['orders.totalAmount']).toBe(800); // 100+200+150+300+50
222
- expect(result.rows[0]['orders.avgAmount']).toBe(160); // 800/5
223
- });
224
-
225
- it('should apply filters', async () => {
226
- const result = await service.query({
227
- cube: 'orders',
228
- measures: ['orders.count', 'orders.totalAmount'],
229
- filters: [
230
- { member: 'orders.status', operator: 'equals', values: ['completed'] }
231
- ]
232
- });
233
-
234
- expect(result.rows).toHaveLength(1);
235
- expect(result.rows[0]['orders.count']).toBe(3);
236
- expect(result.rows[0]['orders.totalAmount']).toBe(600); // 100+200+300
237
- });
238
-
239
- it('should support sorting', async () => {
240
- const result = await service.query({
241
- cube: 'orders',
242
- measures: ['orders.totalAmount'],
243
- dimensions: ['orders.customer'],
244
- order: { 'orders.totalAmount': 'desc' }
245
- });
246
-
247
- expect(result.rows[0]['orders.customer']).toBe('Charlie'); // 300
248
- expect(result.rows[1]['orders.customer']).toBe('Alice'); // 250
249
- expect(result.rows[2]['orders.customer']).toBe('Bob'); // 250
250
- });
251
-
252
- it('should support limit and offset', async () => {
253
- const result = await service.query({
254
- cube: 'orders',
255
- measures: ['orders.count'],
256
- dimensions: ['orders.customer'],
257
- order: { 'orders.customer': 'asc' },
258
- limit: 2,
259
- offset: 1
260
- });
261
-
262
- expect(result.rows).toHaveLength(2);
263
- expect(result.rows[0]['orders.customer']).toBe('Bob');
264
- expect(result.rows[1]['orders.customer']).toBe('Charlie');
265
- });
266
-
267
- it('should throw error for unknown cube', async () => {
268
- await expect(async () => {
269
- await service.query({
270
- cube: 'unknown',
271
- measures: ['unknown.count']
272
- });
273
- }).rejects.toThrow('Cube not found: unknown');
274
- });
275
-
276
- it('should include SQL in result for debugging', async () => {
277
- const result = await service.query({
278
- cube: 'orders',
279
- measures: ['orders.count']
280
- });
281
-
282
- expect(result.sql).toBeDefined();
283
- expect(result.sql).toContain('orders');
284
- });
285
- });
286
-
287
- describe('generateSql', () => {
288
- it('should generate SQL for a simple query', async () => {
289
- const result = await service.generateSql({
290
- cube: 'orders',
291
- measures: ['orders.count']
292
- });
293
-
294
- expect(result.sql).toContain('SELECT');
295
- expect(result.sql).toContain('COUNT(*)');
296
- expect(result.sql).toContain('FROM orders');
297
- });
298
-
299
- it('should generate SQL with GROUP BY', async () => {
300
- const result = await service.generateSql({
301
- cube: 'orders',
302
- measures: ['orders.count'],
303
- dimensions: ['orders.status']
304
- });
305
-
306
- expect(result.sql).toContain('GROUP BY status');
307
- });
308
-
309
- it('should generate SQL with WHERE clause', async () => {
310
- const result = await service.generateSql({
311
- cube: 'orders',
312
- measures: ['orders.count'],
313
- filters: [
314
- { member: 'orders.status', operator: 'equals', values: ['completed'] }
315
- ]
316
- });
317
-
318
- expect(result.sql).toContain('WHERE');
319
- expect(result.sql).toContain('status');
320
- });
321
-
322
- it('should generate SQL with ORDER BY', async () => {
323
- const result = await service.generateSql({
324
- cube: 'orders',
325
- measures: ['orders.count'],
326
- dimensions: ['orders.status'],
327
- order: { 'orders.status': 'asc' }
328
- });
329
-
330
- expect(result.sql).toContain('ORDER BY');
331
- expect(result.sql).toContain('ASC');
332
- });
333
-
334
- it('should generate SQL with LIMIT and OFFSET', async () => {
335
- const result = await service.generateSql({
336
- cube: 'orders',
337
- measures: ['orders.count'],
338
- limit: 10,
339
- offset: 5
340
- });
341
-
342
- expect(result.sql).toContain('LIMIT 10');
343
- expect(result.sql).toContain('OFFSET 5');
344
- });
345
- });
346
- });