@magek/adapter-read-model-store-memory 0.0.10

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.
@@ -0,0 +1,10 @@
1
+ import { MagekConfig, ReadModelInterface, ReadModelStoreAdapter, UUID, FilterFor, SortFor, ProjectionFor, ReadModelListResult, SequenceKey, ReadOnlyNonEmptyArray } from '@magek/common';
2
+ import { MemoryReadModelRegistry } from './memory-read-model-registry';
3
+ declare function fetchReadModel(db: MemoryReadModelRegistry, config: MagekConfig, readModelName: string, readModelID: UUID, sequenceKey?: SequenceKey): Promise<ReadOnlyNonEmptyArray<ReadModelInterface> | undefined>;
4
+ declare function storeReadModel(db: MemoryReadModelRegistry, config: MagekConfig, readModelName: string, readModel: ReadModelInterface, expectedCurrentVersion: number): Promise<void>;
5
+ declare function searchReadModel<TReadModel extends ReadModelInterface>(db: MemoryReadModelRegistry, config: MagekConfig, readModelName: string, filters: FilterFor<unknown>, sortBy?: SortFor<unknown>, limit?: number, afterCursor?: Record<string, string> | undefined, paginatedVersion?: boolean, select?: ProjectionFor<TReadModel>): Promise<Array<TReadModel> | ReadModelListResult<TReadModel>>;
6
+ declare function deleteReadModel(db: MemoryReadModelRegistry, config: MagekConfig, readModelName: string, readModel: ReadModelInterface): Promise<void>;
7
+ export declare const readModelStore: ReadModelStoreAdapter;
8
+ export { MemoryReadModelRegistry } from './memory-read-model-registry';
9
+ export { evaluateFilter, convertFilter } from './library/filter-evaluator';
10
+ export { fetchReadModel, storeReadModel, searchReadModel, deleteReadModel };
package/dist/index.js ADDED
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertFilter = exports.evaluateFilter = exports.MemoryReadModelRegistry = exports.readModelStore = void 0;
4
+ exports.fetchReadModel = fetchReadModel;
5
+ exports.storeReadModel = storeReadModel;
6
+ exports.searchReadModel = searchReadModel;
7
+ exports.deleteReadModel = deleteReadModel;
8
+ const common_1 = require("@magek/common");
9
+ const memory_read_model_registry_1 = require("./memory-read-model-registry");
10
+ // Pre-built Memory Read Model Store Adapter instance
11
+ const readModelRegistry = new memory_read_model_registry_1.MemoryReadModelRegistry();
12
+ async function fetchReadModel(db, config, readModelName, readModelID, sequenceKey) {
13
+ const logger = (0, common_1.getLogger)(config, 'memory-read-model-adapter#fetchReadModel');
14
+ const query = { typeName: readModelName, 'value.id': readModelID };
15
+ // If sequenceKey is provided, add it to the query
16
+ if (sequenceKey) {
17
+ query[`value.${sequenceKey.name}`] = sequenceKey.value;
18
+ }
19
+ const response = await db.query(query);
20
+ if (response.length === 0) {
21
+ logger.debug(`Read model ${readModelName} with ID ${readModelID} not found`);
22
+ return undefined;
23
+ }
24
+ logger.debug(`Loaded read model ${readModelName} with ID ${readModelID} with result:`, response.map((item) => item.value));
25
+ return response.map((item) => item.value);
26
+ }
27
+ async function storeReadModel(db, config, readModelName, readModel, expectedCurrentVersion) {
28
+ const logger = (0, common_1.getLogger)(config, 'memory-read-model-adapter#storeReadModel');
29
+ logger.debug('Storing readModel ' + JSON.stringify(readModel));
30
+ try {
31
+ await db.store({ typeName: readModelName, value: readModel }, expectedCurrentVersion);
32
+ }
33
+ catch (e) {
34
+ if (e instanceof common_1.OptimisticConcurrencyUnexpectedVersionError) {
35
+ logger.warn(`Unique violated storing ReadModel ${JSON.stringify(readModel)} and expectedCurrentVersion ${expectedCurrentVersion}`);
36
+ throw e;
37
+ }
38
+ throw e;
39
+ }
40
+ logger.debug('Read model stored');
41
+ }
42
+ async function searchReadModel(db, config, readModelName, filters, sortBy, limit, afterCursor, paginatedVersion = false, select) {
43
+ const logger = (0, common_1.getLogger)(config, 'memory-read-model-adapter#searchReadModel');
44
+ logger.debug('Converting filter to query');
45
+ const query = { typeName: readModelName, filters };
46
+ logger.debug('Got query ', query);
47
+ const skipId = afterCursor?.id ? parseInt(afterCursor?.id) : 0;
48
+ const result = await db.query(query, sortBy, skipId, limit, select);
49
+ logger.debug('Search result: ', result);
50
+ const items = result?.map((envelope) => envelope.value) ?? [];
51
+ if (paginatedVersion) {
52
+ return {
53
+ items: items,
54
+ count: items?.length ?? 0,
55
+ cursor: { id: ((limit ? limit : 1) + skipId).toString() },
56
+ };
57
+ }
58
+ return items;
59
+ }
60
+ async function deleteReadModel(db, config, readModelName, readModel) {
61
+ const logger = (0, common_1.getLogger)(config, 'memory-read-model-adapter#deleteReadModel');
62
+ logger.debug(`Entering to Read model deleted. ID=${readModel.id}.Name=${readModelName}`);
63
+ try {
64
+ await db.deleteById(readModel.id, readModelName);
65
+ logger.debug(`Read model deleted. ${readModelName} ID = ${readModel.id}`);
66
+ }
67
+ catch (e) {
68
+ logger.warn(`Read model to delete ${readModelName} ID = ${readModel.id} not found`);
69
+ }
70
+ }
71
+ exports.readModelStore = {
72
+ fetch: async (config, readModelName, readModelID, sequenceKey) => {
73
+ const result = await fetchReadModel(readModelRegistry, config, readModelName, readModelID, sequenceKey);
74
+ if (!result || result.length === 0) {
75
+ return undefined;
76
+ }
77
+ return result;
78
+ },
79
+ search: async (config, readModelName, filters, sortBy, limit, afterCursor, paginatedVersion, select) => {
80
+ return await searchReadModel(readModelRegistry, config, readModelName, filters, sortBy, limit, afterCursor, paginatedVersion ?? false, select);
81
+ },
82
+ store: async (config, readModelName, readModel) => {
83
+ const expectedCurrentVersion = (readModel.version ?? 1) - 1;
84
+ await storeReadModel(readModelRegistry, config, readModelName, readModel.value, expectedCurrentVersion);
85
+ // Return the stored envelope with updated timestamps
86
+ return {
87
+ ...readModel,
88
+ updatedAt: new Date().toISOString(),
89
+ };
90
+ },
91
+ delete: async (config, readModelName, readModelID) => {
92
+ // Create a minimal ReadModelInterface for the delete operation
93
+ const readModel = {
94
+ id: readModelID,
95
+ };
96
+ await deleteReadModel(readModelRegistry, config, readModelName, readModel);
97
+ },
98
+ rawToEnvelopes: async (config, rawReadModels) => {
99
+ // This would typically convert raw database records to envelopes
100
+ // For now, assume rawReadModels is already in the correct format
101
+ return rawReadModels;
102
+ },
103
+ healthCheck: {
104
+ isUp: async () => true,
105
+ details: async () => {
106
+ return {
107
+ type: 'memory',
108
+ count: readModelRegistry.getCount(),
109
+ };
110
+ },
111
+ urls: async () => ['memory://in-memory-read-model-store'],
112
+ },
113
+ };
114
+ // Export individual components for backward compatibility and testing
115
+ var memory_read_model_registry_2 = require("./memory-read-model-registry");
116
+ Object.defineProperty(exports, "MemoryReadModelRegistry", { enumerable: true, get: function () { return memory_read_model_registry_2.MemoryReadModelRegistry; } });
117
+ var filter_evaluator_1 = require("./library/filter-evaluator");
118
+ Object.defineProperty(exports, "evaluateFilter", { enumerable: true, get: function () { return filter_evaluator_1.evaluateFilter; } });
119
+ Object.defineProperty(exports, "convertFilter", { enumerable: true, get: function () { return filter_evaluator_1.convertFilter; } });
@@ -0,0 +1,42 @@
1
+ export type FilterValue = number | string | boolean | null | undefined;
2
+ export interface EvaluatedFilter {
3
+ [key: string]: FilterOperation;
4
+ }
5
+ export type FilterOperation = FilterValue | {
6
+ $eq?: FilterValue;
7
+ } | {
8
+ $ne?: FilterValue;
9
+ } | {
10
+ $lt?: FilterValue;
11
+ } | {
12
+ $gt?: FilterValue;
13
+ } | {
14
+ $lte?: FilterValue;
15
+ } | {
16
+ $gte?: FilterValue;
17
+ } | {
18
+ $in?: FilterValue[];
19
+ } | {
20
+ $exists?: boolean;
21
+ } | {
22
+ $regex?: RegExp;
23
+ } | {
24
+ $elemMatch?: FilterValue;
25
+ } | {
26
+ $and?: EvaluatedFilter[];
27
+ } | {
28
+ $or?: EvaluatedFilter[];
29
+ } | {
30
+ $not?: EvaluatedFilter;
31
+ };
32
+ /**
33
+ * Evaluates a filter against a value object.
34
+ * Returns true if the value matches the filter conditions.
35
+ */
36
+ export declare function evaluateFilter(value: Record<string, any>, filters: any): boolean;
37
+ /**
38
+ * Converts a GraphQL filter to an evaluated filter structure.
39
+ * This function transforms Magek-style filters into a format that can be
40
+ * efficiently evaluated against data.
41
+ */
42
+ export declare function convertFilter(filters: any, prefix?: string): EvaluatedFilter;
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.evaluateFilter = evaluateFilter;
5
+ exports.convertFilter = convertFilter;
6
+ /**
7
+ * Evaluates a filter against a value object.
8
+ * Returns true if the value matches the filter conditions.
9
+ */
10
+ function evaluateFilter(value, filters) {
11
+ if (!filters || Object.keys(filters).length === 0) {
12
+ return true;
13
+ }
14
+ for (const key in filters) {
15
+ const filterValue = filters[key];
16
+ // Handle logical operators
17
+ if (key === 'and') {
18
+ const andFilters = filterValue;
19
+ if (!andFilters.every((f) => evaluateFilter(value, f))) {
20
+ return false;
21
+ }
22
+ continue;
23
+ }
24
+ if (key === 'or') {
25
+ const orFilters = filterValue;
26
+ if (!orFilters.some((f) => evaluateFilter(value, f))) {
27
+ return false;
28
+ }
29
+ continue;
30
+ }
31
+ if (key === 'not') {
32
+ if (evaluateFilter(value, filterValue)) {
33
+ return false;
34
+ }
35
+ continue;
36
+ }
37
+ // Handle field-level filters
38
+ const fieldValue = getNestedValue(value, key);
39
+ if (!evaluateFieldFilter(fieldValue, filterValue)) {
40
+ return false;
41
+ }
42
+ }
43
+ return true;
44
+ }
45
+ /**
46
+ * Gets a nested value from an object using dot notation.
47
+ */
48
+ function getNestedValue(obj, path) {
49
+ const parts = path.split('.');
50
+ let current = obj;
51
+ for (const part of parts) {
52
+ if (current === null || current === undefined) {
53
+ return undefined;
54
+ }
55
+ current = current[part];
56
+ }
57
+ return current;
58
+ }
59
+ /**
60
+ * Evaluates a filter against a single field value.
61
+ */
62
+ function evaluateFieldFilter(fieldValue, filter) {
63
+ if (filter === null || filter === undefined) {
64
+ return true;
65
+ }
66
+ // If filter is a primitive, it's an implicit eq operation
67
+ if (typeof filter !== 'object') {
68
+ return fieldValue === filter;
69
+ }
70
+ // Check for filter operations
71
+ const filterKeys = Object.keys(filter);
72
+ for (const op of filterKeys) {
73
+ const opValue = filter[op];
74
+ switch (op) {
75
+ case 'eq':
76
+ if (fieldValue !== opValue)
77
+ return false;
78
+ break;
79
+ case 'ne':
80
+ if (fieldValue === opValue)
81
+ return false;
82
+ break;
83
+ case 'lt':
84
+ if (fieldValue >= opValue)
85
+ return false;
86
+ break;
87
+ case 'gt':
88
+ if (fieldValue <= opValue)
89
+ return false;
90
+ break;
91
+ case 'lte':
92
+ if (fieldValue > opValue)
93
+ return false;
94
+ break;
95
+ case 'gte':
96
+ if (fieldValue < opValue)
97
+ return false;
98
+ break;
99
+ case 'in':
100
+ if (!Array.isArray(opValue) || !opValue.includes(fieldValue))
101
+ return false;
102
+ break;
103
+ case 'isDefined': {
104
+ const exists = fieldValue !== undefined && fieldValue !== null;
105
+ if (opValue !== exists)
106
+ return false;
107
+ break;
108
+ }
109
+ case 'contains':
110
+ if (typeof fieldValue !== 'string' || !fieldValue.includes(opValue))
111
+ return false;
112
+ break;
113
+ case 'beginsWith':
114
+ if (typeof fieldValue !== 'string' || !fieldValue.startsWith(opValue))
115
+ return false;
116
+ break;
117
+ case 'regex':
118
+ if (typeof fieldValue !== 'string')
119
+ return false;
120
+ try {
121
+ const regex = new RegExp(opValue);
122
+ if (!regex.test(fieldValue))
123
+ return false;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ break;
129
+ case 'iRegex':
130
+ if (typeof fieldValue !== 'string')
131
+ return false;
132
+ try {
133
+ const regex = new RegExp(opValue, 'i');
134
+ if (!regex.test(fieldValue))
135
+ return false;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ break;
141
+ case 'includes':
142
+ if (!Array.isArray(fieldValue))
143
+ return false;
144
+ if (typeof opValue === 'string') {
145
+ // Check if any element contains the string
146
+ if (!fieldValue.some((item) => typeof item === 'string' && item.includes(opValue)))
147
+ return false;
148
+ }
149
+ else {
150
+ // Check if array includes the value (elemMatch-like)
151
+ if (!fieldValue.some((item) => {
152
+ if (typeof opValue === 'object' && opValue !== null) {
153
+ return evaluateFilter(item, opValue);
154
+ }
155
+ return item === opValue;
156
+ }))
157
+ return false;
158
+ }
159
+ break;
160
+ default:
161
+ // If the key is not a known operator, it might be a nested filter
162
+ if (!evaluateFieldFilter(getNestedValue(fieldValue, op), filter[op])) {
163
+ return false;
164
+ }
165
+ }
166
+ }
167
+ return true;
168
+ }
169
+ /**
170
+ * Converts a GraphQL filter to an evaluated filter structure.
171
+ * This function transforms Magek-style filters into a format that can be
172
+ * efficiently evaluated against data.
173
+ */
174
+ function convertFilter(filters, prefix = '') {
175
+ const result = {};
176
+ for (const key in filters) {
177
+ const filterValue = filters[key];
178
+ const fullKey = prefix ? `${prefix}.${key}` : key;
179
+ if (key === 'and' || key === 'or') {
180
+ const logicalFilters = filterValue.map((f) => convertFilter(f));
181
+ result[`$${key}`] = logicalFilters;
182
+ }
183
+ else if (key === 'not') {
184
+ result['$not'] = convertFilter(filterValue);
185
+ }
186
+ else if (typeof filterValue === 'object' && filterValue !== null) {
187
+ // Check if this is a filter operation object
188
+ const opKeys = Object.keys(filterValue);
189
+ const isFilterOp = opKeys.some((k) => ['eq', 'ne', 'lt', 'gt', 'lte', 'gte', 'in', 'isDefined', 'contains', 'beginsWith', 'regex', 'iRegex', 'includes'].includes(k));
190
+ if (isFilterOp) {
191
+ result[fullKey] = convertFilterOperation(filterValue);
192
+ }
193
+ else {
194
+ // Nested object filter
195
+ Object.assign(result, convertFilter(filterValue, fullKey));
196
+ }
197
+ }
198
+ else {
199
+ result[fullKey] = filterValue;
200
+ }
201
+ }
202
+ return result;
203
+ }
204
+ function convertFilterOperation(filter) {
205
+ const op = Object.keys(filter)[0];
206
+ const value = filter[op];
207
+ switch (op) {
208
+ case 'eq':
209
+ return { $eq: value };
210
+ case 'ne':
211
+ return { $ne: value };
212
+ case 'lt':
213
+ return { $lt: value };
214
+ case 'gt':
215
+ return { $gt: value };
216
+ case 'lte':
217
+ return { $lte: value };
218
+ case 'gte':
219
+ return { $gte: value };
220
+ case 'in':
221
+ return { $in: value };
222
+ case 'isDefined':
223
+ return { $exists: value };
224
+ case 'contains':
225
+ case 'beginsWith':
226
+ case 'regex':
227
+ return { $regex: new RegExp(op === 'beginsWith' ? `^${value}` : value) };
228
+ case 'iRegex':
229
+ return { $regex: new RegExp(value, 'i') };
230
+ case 'includes':
231
+ if (typeof value === 'string') {
232
+ return { $regex: new RegExp(value) };
233
+ }
234
+ return { $elemMatch: value };
235
+ default:
236
+ return value;
237
+ }
238
+ }
@@ -0,0 +1,27 @@
1
+ import { ReadModelEnvelope, SortFor, ProjectionFor, UUID } from '@magek/common';
2
+ export interface ReadModelStoreEntry extends ReadModelEnvelope {
3
+ uniqueKey: string;
4
+ }
5
+ export declare class MemoryReadModelRegistry {
6
+ private readModels;
7
+ private byType;
8
+ private getKey;
9
+ query(query: QueryFilter, sortBy?: SortFor<unknown>, skip?: number, limit?: number, select?: ProjectionFor<unknown>): Promise<Array<ReadModelEnvelope>>;
10
+ store(readModel: ReadModelEnvelope, expectedCurrentVersion: number): Promise<void>;
11
+ deleteById(id: UUID, typeName: string): Promise<number>;
12
+ deleteAll(): Promise<number>;
13
+ count(query?: QueryFilter): Promise<number>;
14
+ getCount(): number;
15
+ private matchesFilter;
16
+ private getNestedValue;
17
+ private toLocalSortFor;
18
+ private sortResults;
19
+ private filterFields;
20
+ private setNestedValue;
21
+ }
22
+ export interface QueryFilter {
23
+ typeName?: string;
24
+ 'value.id'?: UUID;
25
+ filters?: any;
26
+ [key: string]: any;
27
+ }
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryReadModelRegistry = void 0;
4
+ const common_1 = require("@magek/common");
5
+ const filter_evaluator_1 = require("./library/filter-evaluator");
6
+ class MemoryReadModelRegistry {
7
+ readModels = new Map();
8
+ byType = new Map();
9
+ getKey(typeName, id) {
10
+ return `${typeName}:${id}`;
11
+ }
12
+ async query(query, sortBy, skip, limit, select) {
13
+ let results = [];
14
+ // Filter by typeName first if provided
15
+ if (query.typeName) {
16
+ const typeKeys = this.byType.get(query.typeName);
17
+ if (typeKeys) {
18
+ for (const key of Array.from(typeKeys)) {
19
+ const entry = this.readModels.get(key);
20
+ if (entry && this.matchesFilter(entry, query)) {
21
+ results.push(entry);
22
+ }
23
+ }
24
+ }
25
+ }
26
+ else {
27
+ // Query all read models
28
+ for (const entry of Array.from(this.readModels.values())) {
29
+ if (this.matchesFilter(entry, query)) {
30
+ results.push(entry);
31
+ }
32
+ }
33
+ }
34
+ // Apply sorting
35
+ if (sortBy && Object.keys(sortBy).length > 0) {
36
+ const sortedList = this.toLocalSortFor(sortBy);
37
+ if (sortedList) {
38
+ results = this.sortResults(results, sortedList);
39
+ }
40
+ }
41
+ // Apply skip
42
+ if (skip && skip > 0) {
43
+ results = results.slice(skip);
44
+ }
45
+ // Apply limit
46
+ if (limit && limit > 0) {
47
+ results = results.slice(0, limit);
48
+ }
49
+ // Apply select (field projection)
50
+ if (select && select.length > 0) {
51
+ results = results.map((result) => ({
52
+ ...result,
53
+ value: this.filterFields(result.value, select),
54
+ }));
55
+ }
56
+ return results;
57
+ }
58
+ async store(readModel, expectedCurrentVersion) {
59
+ const key = this.getKey(readModel.typeName, readModel.value.id);
60
+ const version = readModel.value.magekMetadata?.version ?? 1;
61
+ const uniqueKey = `${readModel.typeName}_${readModel.value.id}_${version}`;
62
+ const entry = {
63
+ ...readModel,
64
+ uniqueKey,
65
+ };
66
+ if (version === 1) {
67
+ // Insert new read model
68
+ this.readModels.set(key, entry);
69
+ // Index by type
70
+ if (!this.byType.has(readModel.typeName)) {
71
+ this.byType.set(readModel.typeName, new Set());
72
+ }
73
+ this.byType.get(readModel.typeName).add(key);
74
+ }
75
+ else {
76
+ // Update existing read model with optimistic concurrency check
77
+ const existing = this.readModels.get(key);
78
+ if (!existing) {
79
+ throw new common_1.OptimisticConcurrencyUnexpectedVersionError(`Can't update readModel ${JSON.stringify(readModel)} with expectedCurrentVersion = ${expectedCurrentVersion}. Read model not found.`);
80
+ }
81
+ const existingVersion = existing.value.magekMetadata?.version ?? 0;
82
+ if (existingVersion !== expectedCurrentVersion) {
83
+ throw new common_1.OptimisticConcurrencyUnexpectedVersionError(`Can't update readModel ${JSON.stringify(readModel)} with expectedCurrentVersion = ${expectedCurrentVersion}. Current version is ${existingVersion}.`);
84
+ }
85
+ this.readModels.set(key, entry);
86
+ }
87
+ }
88
+ async deleteById(id, typeName) {
89
+ const key = this.getKey(typeName, id);
90
+ const exists = this.readModels.has(key);
91
+ if (exists) {
92
+ this.readModels.delete(key);
93
+ this.byType.get(typeName)?.delete(key);
94
+ return 1;
95
+ }
96
+ return 0;
97
+ }
98
+ async deleteAll() {
99
+ const count = this.readModels.size;
100
+ this.readModels.clear();
101
+ this.byType.clear();
102
+ return count;
103
+ }
104
+ async count(query) {
105
+ if (!query) {
106
+ return this.readModels.size;
107
+ }
108
+ let count = 0;
109
+ for (const entry of Array.from(this.readModels.values())) {
110
+ if (this.matchesFilter(entry, query)) {
111
+ count++;
112
+ }
113
+ }
114
+ return count;
115
+ }
116
+ getCount() {
117
+ return this.readModels.size;
118
+ }
119
+ matchesFilter(entry, query) {
120
+ // Check typeName
121
+ if (query.typeName && entry.typeName !== query.typeName) {
122
+ return false;
123
+ }
124
+ // Check value.id
125
+ if (query['value.id'] && entry.value.id !== query['value.id']) {
126
+ return false;
127
+ }
128
+ // Check sequence key if present
129
+ for (const key of Object.keys(query)) {
130
+ if (key.startsWith('value.') && key !== 'value.id') {
131
+ const fieldPath = key.substring(6); // Remove 'value.' prefix
132
+ const fieldValue = this.getNestedValue(entry.value, fieldPath);
133
+ if (fieldValue !== query[key]) {
134
+ return false;
135
+ }
136
+ }
137
+ }
138
+ // Check GraphQL-style filters
139
+ if (query.filters) {
140
+ if (!(0, filter_evaluator_1.evaluateFilter)(entry.value, query.filters)) {
141
+ return false;
142
+ }
143
+ }
144
+ return true;
145
+ }
146
+ getNestedValue(obj, path) {
147
+ const parts = path.split('.');
148
+ let current = obj;
149
+ for (const part of parts) {
150
+ if (current === null || current === undefined) {
151
+ return undefined;
152
+ }
153
+ current = current[part];
154
+ }
155
+ return current;
156
+ }
157
+ toLocalSortFor(sortBy, parentKey = '', sortedList = Object.create(null)) {
158
+ if (!sortBy || Object.keys(sortBy).length === 0)
159
+ return undefined;
160
+ Object.entries(sortBy).forEach(([key, value]) => {
161
+ if (typeof value === 'string') {
162
+ sortedList[`value.${parentKey}${key}`] = value === 'ASC' ? 1 : -1;
163
+ }
164
+ else {
165
+ this.toLocalSortFor(value, `${parentKey}${key}.`, sortedList);
166
+ }
167
+ });
168
+ return sortedList;
169
+ }
170
+ sortResults(results, sortedList) {
171
+ return [...results].sort((a, b) => {
172
+ for (const [path, direction] of Object.entries(sortedList)) {
173
+ const aValue = this.getNestedValue(a, path);
174
+ const bValue = this.getNestedValue(b, path);
175
+ if (aValue < bValue)
176
+ return -1 * direction;
177
+ if (aValue > bValue)
178
+ return 1 * direction;
179
+ }
180
+ return 0;
181
+ });
182
+ }
183
+ filterFields(obj, select) {
184
+ const result = Object.create(null);
185
+ select.forEach((field) => {
186
+ const parts = field.split('.');
187
+ this.setNestedValue(result, obj, parts);
188
+ });
189
+ return result;
190
+ }
191
+ setNestedValue(result, source, parts) {
192
+ let currentResult = result;
193
+ let currentSource = source;
194
+ for (let i = 0; i < parts.length; i++) {
195
+ const part = parts[i];
196
+ const isLast = i === parts.length - 1;
197
+ if (part.endsWith('[]')) {
198
+ const arrayField = part.slice(0, -2);
199
+ if (!Array.isArray(currentSource[arrayField])) {
200
+ return;
201
+ }
202
+ if (!currentResult[arrayField]) {
203
+ currentResult[arrayField] = [];
204
+ }
205
+ if (isLast) {
206
+ currentResult[arrayField] = currentSource[arrayField];
207
+ }
208
+ else {
209
+ currentSource[arrayField].forEach((item, index) => {
210
+ if (!currentResult[arrayField][index]) {
211
+ currentResult[arrayField][index] = Object.create(null);
212
+ }
213
+ this.setNestedValue(currentResult[arrayField][index], item, parts.slice(i + 1));
214
+ });
215
+ }
216
+ }
217
+ else {
218
+ if (isLast) {
219
+ if (currentSource[part] !== undefined) {
220
+ currentResult[part] = currentSource[part];
221
+ }
222
+ }
223
+ else {
224
+ if (!currentSource[part]) {
225
+ return;
226
+ }
227
+ if (!currentResult[part]) {
228
+ currentResult[part] = Array.isArray(currentSource[part]) ? [] : Object.create(null);
229
+ }
230
+ currentResult = currentResult[part];
231
+ currentSource = currentSource[part];
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ exports.MemoryReadModelRegistry = MemoryReadModelRegistry;
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@magek/adapter-read-model-store-memory",
3
+ "version": "0.0.10",
4
+ "description": "In-memory read model store adapter for the Magek framework",
5
+ "keywords": [
6
+ "read-model-store",
7
+ "memory",
8
+ "in-memory"
9
+ ],
10
+ "author": "Boosterin Labs SLU",
11
+ "homepage": "https://magek.ai",
12
+ "license": "Apache-2.0",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "main": "dist/index.js",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/theam/magek.git"
23
+ },
24
+ "engines": {
25
+ "node": ">=22.0.0 <23.0.0"
26
+ },
27
+ "dependencies": {
28
+ "@magek/common": "workspace:^0.0.10",
29
+ "tslib": "2.8.1"
30
+ },
31
+ "scripts": {
32
+ "format": "prettier --write --ext '.js,.ts' **/*.ts **/*/*.ts",
33
+ "lint:check": "eslint \"**/*.ts\"",
34
+ "lint:fix": "eslint --quiet --fix \"**/*.ts\"",
35
+ "build": "tsc -b tsconfig.json",
36
+ "clean": "rimraf ./dist tsconfig.tsbuildinfo",
37
+ "prepack": "tsc -b tsconfig.json",
38
+ "test": "tsc --noEmit -p tsconfig.test.json && c8 mocha --forbid-only \"test/**/*.test.ts\""
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/theam/magek/issues"
42
+ },
43
+ "devDependencies": {
44
+ "@magek/eslint-config": "workspace:^0.0.10",
45
+ "@types/chai": "5.2.3",
46
+ "@types/chai-as-promised": "8.0.2",
47
+ "@types/mocha": "10.0.10",
48
+ "@types/node": "22.19.8",
49
+ "@types/sinon": "21.0.0",
50
+ "@types/sinon-chai": "4.0.0",
51
+ "chai": "6.2.2",
52
+ "chai-as-promised": "8.0.2",
53
+ "@faker-js/faker": "10.2.0",
54
+ "mocha": "11.7.5",
55
+ "c8": "^10.1.3",
56
+ "rimraf": "6.1.2",
57
+ "sinon": "21.0.1",
58
+ "sinon-chai": "4.0.1",
59
+ "tsx": "^4.19.2",
60
+ "typescript": "5.9.3"
61
+ }
62
+ }