@routier/core 0.0.1-alpha.1

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.
Files changed (168) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +91 -0
  3. package/rspack.config.mjs +65 -0
  4. package/src/assertions/index.ts +37 -0
  5. package/src/codegen/SlotPath.ts +20 -0
  6. package/src/codegen/blocks.ts +578 -0
  7. package/src/codegen/handlers/CloneHandlerBuilder.ts +13 -0
  8. package/src/codegen/handlers/CompareHandlerBuilder.ts +13 -0
  9. package/src/codegen/handlers/DeserializeHandlerBuilder.ts +20 -0
  10. package/src/codegen/handlers/EnableChangeTrackingHandlerBuilder.ts +13 -0
  11. package/src/codegen/handlers/EnrichmentHandlerBuilder.ts +28 -0
  12. package/src/codegen/handlers/FreezeHandlerBuilder.ts +13 -0
  13. package/src/codegen/handlers/HashHandlerBuilder.ts +25 -0
  14. package/src/codegen/handlers/HashTypeHandlerBuilder.ts +11 -0
  15. package/src/codegen/handlers/IdSelectorHandlerBuilder.ts +10 -0
  16. package/src/codegen/handlers/MergeHandlerBuilder.ts +19 -0
  17. package/src/codegen/handlers/PrepareHandlerBuilder.ts +23 -0
  18. package/src/codegen/handlers/SerializeHandlerBuilder.ts +15 -0
  19. package/src/codegen/handlers/StripHandlerBuilder.ts +18 -0
  20. package/src/codegen/handlers/clone/CloneObjectHandler.ts +29 -0
  21. package/src/codegen/handlers/clone/CloneValueHandler.ts +33 -0
  22. package/src/codegen/handlers/compare/CompareObjectHandler.ts +15 -0
  23. package/src/codegen/handlers/compare/CompareValueHandler.ts +27 -0
  24. package/src/codegen/handlers/deserialize/DeserializeComputedValueHandler.ts +16 -0
  25. package/src/codegen/handlers/deserialize/DeserializeDateHandler.ts +45 -0
  26. package/src/codegen/handlers/deserialize/DeserializeFunctionHandler.ts +16 -0
  27. package/src/codegen/handlers/deserialize/DeserializeObjectHandler.ts +29 -0
  28. package/src/codegen/handlers/deserialize/DeserializeValueHandler.ts +33 -0
  29. package/src/codegen/handlers/enableChangeTracking/EnableChangeTrackingObjectHandler.ts +20 -0
  30. package/src/codegen/handlers/enableChangeTracking/EnableChangeTrackingPrimitiveValueHandler.ts +15 -0
  31. package/src/codegen/handlers/enrichment/EnrichmentComputedValueHandler.ts +37 -0
  32. package/src/codegen/handlers/enrichment/EnrichmentDefaultFunctionHandler.ts +50 -0
  33. package/src/codegen/handlers/enrichment/EnrichmentDefaultValueHandler.ts +18 -0
  34. package/src/codegen/handlers/enrichment/EnrichmentFunctionHandler.ts +38 -0
  35. package/src/codegen/handlers/enrichment/EnrichmentNullableObjectHandler.ts +44 -0
  36. package/src/codegen/handlers/enrichment/EnrichmentObjectHandler.ts +29 -0
  37. package/src/codegen/handlers/enrichment/EnrichmentObjectIdentityHandler.ts +16 -0
  38. package/src/codegen/handlers/enrichment/EnrichmentPrimitiveHandler.ts +13 -0
  39. package/src/codegen/handlers/enrichment/EnrichmentPrimitiveIdentityHandler.ts +21 -0
  40. package/src/codegen/handlers/freeze/FreezeObjectHandler.ts +20 -0
  41. package/src/codegen/handlers/freeze/FreezePrimitiveValueHandler.ts +15 -0
  42. package/src/codegen/handlers/hash/HashComputedValueHandler.ts +15 -0
  43. package/src/codegen/handlers/hash/HashDateHandler.ts +26 -0
  44. package/src/codegen/handlers/hash/HashFunctionHandler.ts +16 -0
  45. package/src/codegen/handlers/hash/HashIdentityHandler.ts +15 -0
  46. package/src/codegen/handlers/hash/HashKeyHandler.ts +26 -0
  47. package/src/codegen/handlers/hash/HashValueHandler.ts +26 -0
  48. package/src/codegen/handlers/hashType/HashTypeValueHandler.ts +20 -0
  49. package/src/codegen/handlers/idSelector/IdSelectorValueHandler.ts +28 -0
  50. package/src/codegen/handlers/index.ts +13 -0
  51. package/src/codegen/handlers/merge/MergeComputedValueHandler.ts +37 -0
  52. package/src/codegen/handlers/merge/MergeDefaultFunctionHandler.ts +40 -0
  53. package/src/codegen/handlers/merge/MergeDefaultValueHandler.ts +32 -0
  54. package/src/codegen/handlers/merge/MergeFunctionHandler.ts +16 -0
  55. package/src/codegen/handlers/merge/MergePrimitiveHandler.ts +21 -0
  56. package/src/codegen/handlers/prepare/PrepareComputedValueHandler.ts +16 -0
  57. package/src/codegen/handlers/prepare/PrepareFunctionHandler.ts +16 -0
  58. package/src/codegen/handlers/prepare/PrepareIdentityHandler.ts +21 -0
  59. package/src/codegen/handlers/prepare/PrepareKeyHandler.ts +21 -0
  60. package/src/codegen/handlers/prepare/PrepareObjectHandler.ts +38 -0
  61. package/src/codegen/handlers/prepare/PrepareValueHandler.ts +36 -0
  62. package/src/codegen/handlers/serialize/SerializeDateHandler.ts +45 -0
  63. package/src/codegen/handlers/serialize/SerializeObjectHandler.ts +28 -0
  64. package/src/codegen/handlers/serialize/SerializeValueHandler.ts +33 -0
  65. package/src/codegen/handlers/strip/StripIdentityHandler.ts +15 -0
  66. package/src/codegen/handlers/strip/StripKeyHandler.ts +15 -0
  67. package/src/codegen/handlers/strip/StripObjectHandler.ts +35 -0
  68. package/src/codegen/handlers/strip/StripValueHandler.ts +36 -0
  69. package/src/codegen/handlers/types.ts +119 -0
  70. package/src/codegen/index.ts +2 -0
  71. package/src/codegen/types.ts +2 -0
  72. package/src/codegen/utils.ts +74 -0
  73. package/src/collections/Changes.test.ts +337 -0
  74. package/src/collections/Changes.ts +177 -0
  75. package/src/collections/IdSet.ts +28 -0
  76. package/src/collections/MemoryDataCollection.test.ts +424 -0
  77. package/src/collections/MemoryDataCollection.ts +134 -0
  78. package/src/collections/SchemaCollection.ts +22 -0
  79. package/src/collections/TagCollection.test.ts +443 -0
  80. package/src/collections/TagCollection.ts +62 -0
  81. package/src/collections/index.ts +5 -0
  82. package/src/errors/SchemaError.ts +6 -0
  83. package/src/errors/index.ts +1 -0
  84. package/src/expressions/index.ts +3 -0
  85. package/src/expressions/parser.test.ts +913 -0
  86. package/src/expressions/parser.ts +661 -0
  87. package/src/expressions/types.ts +184 -0
  88. package/src/expressions/utils.test.ts +346 -0
  89. package/src/expressions/utils.ts +59 -0
  90. package/src/index.ts +12 -0
  91. package/src/performance/index.ts +26 -0
  92. package/src/pipeline/SyncronousQueue.test.ts +269 -0
  93. package/src/pipeline/SyncronousQueue.ts +30 -0
  94. package/src/pipeline/TrampolinePipeline.test.ts +374 -0
  95. package/src/pipeline/TrampolinePipeline.ts +437 -0
  96. package/src/pipeline/index.ts +2 -0
  97. package/src/plugins/EphemeralDataPlugin.ts +132 -0
  98. package/src/plugins/capabilities/DbPluginCapability.ts +111 -0
  99. package/src/plugins/capabilities/index.ts +2 -0
  100. package/src/plugins/capabilities/logging/DbPluginLoggingCapability.ts +261 -0
  101. package/src/plugins/capabilities/logging/index.ts +1 -0
  102. package/src/plugins/index.ts +6 -0
  103. package/src/plugins/query/Query.ts +67 -0
  104. package/src/plugins/query/QueryOptionsCollection.test.ts +86 -0
  105. package/src/plugins/query/QueryOptionsCollection.ts +154 -0
  106. package/src/plugins/query/index.ts +3 -0
  107. package/src/plugins/query/types.ts +46 -0
  108. package/src/plugins/replication/OptimisticReplicationDbPlugin.ts +202 -0
  109. package/src/plugins/replication/ReplicationDbPlugin.ts +137 -0
  110. package/src/plugins/replication/index.ts +3 -0
  111. package/src/plugins/replication/types.ts +5 -0
  112. package/src/plugins/translators/DataTranslator.ts +46 -0
  113. package/src/plugins/translators/JsonTranslator.test.ts +618 -0
  114. package/src/plugins/translators/JsonTranslator.ts +211 -0
  115. package/src/plugins/translators/SqlTranslator.ts +45 -0
  116. package/src/plugins/translators/index.ts +3 -0
  117. package/src/plugins/types.ts +114 -0
  118. package/src/results/Result.ts +91 -0
  119. package/src/results/index.ts +3 -0
  120. package/src/results/types.ts +17 -0
  121. package/src/results/utils.ts +15 -0
  122. package/src/schema/PropertyInfo.test.ts +479 -0
  123. package/src/schema/PropertyInfo.ts +374 -0
  124. package/src/schema/SchemaDefinition.ts +626 -0
  125. package/src/schema/builder.ts +18 -0
  126. package/src/schema/index.ts +7 -0
  127. package/src/schema/property/base/SchemaBase.ts +49 -0
  128. package/src/schema/property/base/index.ts +1 -0
  129. package/src/schema/property/modifiers/SchemaDefault.ts +29 -0
  130. package/src/schema/property/modifiers/SchemaDeserialize.ts +23 -0
  131. package/src/schema/property/modifiers/SchemaDistinct.ts +14 -0
  132. package/src/schema/property/modifiers/SchemaFrom.ts +54 -0
  133. package/src/schema/property/modifiers/SchemaIdentity.ts +13 -0
  134. package/src/schema/property/modifiers/SchemaIndex.ts +60 -0
  135. package/src/schema/property/modifiers/SchemaKey.ts +28 -0
  136. package/src/schema/property/modifiers/SchemaNullable.ts +18 -0
  137. package/src/schema/property/modifiers/SchemaOptional.ts +19 -0
  138. package/src/schema/property/modifiers/SchemaReadonly.ts +34 -0
  139. package/src/schema/property/modifiers/SchemaSerialize.ts +18 -0
  140. package/src/schema/property/modifiers/SchemaTracked.ts +14 -0
  141. package/src/schema/property/modifiers/index.ts +12 -0
  142. package/src/schema/property/types/SchemaArray.ts +50 -0
  143. package/src/schema/property/types/SchemaBoolean.ts +59 -0
  144. package/src/schema/property/types/SchemaDate.ts +58 -0
  145. package/src/schema/property/types/SchemaNumber.ts +69 -0
  146. package/src/schema/property/types/SchemaObject.ts +44 -0
  147. package/src/schema/property/types/SchemaString.ts +69 -0
  148. package/src/schema/property/types/index.ts +6 -0
  149. package/src/schema/table/SchemaComputed.ts +20 -0
  150. package/src/schema/table/SchemaFunction.ts +15 -0
  151. package/src/schema/table/index.ts +2 -0
  152. package/src/schema/types.ts +238 -0
  153. package/src/types/index.ts +5 -0
  154. package/src/utilities/arrays.test.ts +312 -0
  155. package/src/utilities/arrays.ts +12 -0
  156. package/src/utilities/dates.test.ts +388 -0
  157. package/src/utilities/dates.ts +11 -0
  158. package/src/utilities/dbPluginEventUtils.ts +44 -0
  159. package/src/utilities/index.ts +10 -0
  160. package/src/utilities/objects.ts +7 -0
  161. package/src/utilities/queryOptionsCollection.ts +16 -0
  162. package/src/utilities/replication.ts +23 -0
  163. package/src/utilities/runtime.ts +3 -0
  164. package/src/utilities/strings.ts +18 -0
  165. package/src/utilities/types.ts +1 -0
  166. package/src/utilities/uuid.ts +56 -0
  167. package/tsconfig.json +28 -0
  168. package/vitest.config.ts +11 -0
@@ -0,0 +1,261 @@
1
+ import { IDbPlugin, DbPluginQueryEvent, DbPluginBulkPersistEvent, DbPluginEvent } from "../../../plugins/types";
2
+ import { DbPluginCapability, IDbPluginCapability } from "../DbPluginCapability";
3
+ import { PluginEventResultType, PluginEventPartialResultType } from "../../../results";
4
+ import { now } from "../../../performance";
5
+ import { BulkPersistChanges, BulkPersistResult } from "../../../collections";
6
+
7
+ export class DbPluginLoggingCapability implements IDbPluginCapability {
8
+ private logStyle: 'minimal' | 'detailed' | 'redux' = 'redux';
9
+ private maxLogEntries: number = 100;
10
+ private logHistory: Array<{ type: string; timestamp: number; data: any }> = [];
11
+ private queryPerformance: Map<string, number> = new Map<string, number>();
12
+
13
+ constructor(options?: { logStyle?: 'minimal' | 'detailed' | 'redux'; maxLogEntries?: number }) {
14
+ this.logStyle = options?.logStyle ?? 'redux';
15
+ this.maxLogEntries = options?.maxLogEntries ?? 100;
16
+ }
17
+
18
+ apply<T extends IDbPlugin>(plugin: T) {
19
+ const baseCapability = new DbPluginCapability();
20
+
21
+ const pluginName = plugin.constructor.name;
22
+
23
+ // Query logging
24
+ baseCapability
25
+ .add("queryStart", (event: DbPluginQueryEvent<any, any>) => {
26
+ this.logReduxAction('QUERY_REQUEST', event.id, {
27
+ plugin: pluginName,
28
+ collection: event.operation.schema.collectionName,
29
+ schemaId: event.operation.schema.id,
30
+ changeTracking: event.operation.changeTracking
31
+ }, {
32
+ timestamp: new Date().toISOString(),
33
+ options: this.extractQueryOptions(event.operation)
34
+ });
35
+ this.addToHistory('QUERY_REQUEST', event);
36
+ this.queryPerformance.set(event.id, now());
37
+ })
38
+ .add("queryComplete", (result: PluginEventResultType<any>) => {
39
+ const start = this.queryPerformance.get(result.id);
40
+ this.queryPerformance.delete(result.id)
41
+ const end = now();
42
+ const duration = start == null ? -1 : end - start;
43
+ const performance = this.getPerformanceIndicator(duration);
44
+
45
+ if (result.ok === 'success') {
46
+ this.logReduxAction('QUERY_SUCCESS', result.id, {
47
+ plugin: pluginName,
48
+ resultCount: this.getResultCount(result.data),
49
+ resultType: this.getResultType(result.data)
50
+ }, {
51
+ duration: `${duration.toFixed(4)}ms`,
52
+ performance: performance.label,
53
+ timestamp: new Date().toISOString()
54
+ });
55
+ } else {
56
+ this.logReduxAction('QUERY_ERROR', result.id, {
57
+ plugin: pluginName,
58
+ error: result.error?.message || result.error,
59
+ isCritical: false
60
+ }, {
61
+ duration: `${duration.toFixed(4)}ms`,
62
+ performance: performance.label,
63
+ timestamp: new Date().toISOString()
64
+ });
65
+ }
66
+ this.addToHistory('QUERY_RESULT', { result, duration });
67
+ });
68
+
69
+ // Bulk operations logging
70
+ baseCapability
71
+ .add("bulkPersistStart", (event: DbPluginBulkPersistEvent) => {
72
+ const totalOperations = event.operation.aggregate.size;
73
+ this.logReduxAction('BULK_OPERATIONS_REQUEST', event.id, {
74
+ plugin: pluginName,
75
+ totalOperations,
76
+ schemaCount: event.schemas.size,
77
+ operations: this.extractBulkOperations(event.operation)
78
+ }, {
79
+ timestamp: new Date().toISOString()
80
+ });
81
+ this.addToHistory('BULK_OPERATIONS_REQUEST', event);
82
+ this.queryPerformance.set(event.id, now());
83
+ })
84
+ .add("bulkPersistComplete", (result: PluginEventPartialResultType<BulkPersistResult>) => {
85
+ const start = this.queryPerformance.get(result.id);
86
+ this.queryPerformance.delete(result.id)
87
+ const end = now();
88
+ const duration = start == null ? -1 : end - start;
89
+ const performance = this.getPerformanceIndicator(duration);
90
+
91
+ if (result.ok === 'success') {
92
+ this.logReduxAction('BULK_OPERATIONS_SUCCESS', result.id, {
93
+ plugin: pluginName,
94
+ completedOperations: this.countCompletedOperations(result.data),
95
+ schemaCount: result.data.size
96
+ }, {
97
+ duration: `${duration.toFixed(4)}ms`,
98
+ performance: performance.label,
99
+ timestamp: new Date().toISOString()
100
+ });
101
+ } else {
102
+ this.logReduxAction('BULK_OPERATIONS_ERROR', result.id, {
103
+ plugin: pluginName,
104
+ error: result.error?.message || result.error,
105
+ isCritical: false
106
+ }, {
107
+ duration: `${duration.toFixed(4)}ms`,
108
+ performance: performance.label,
109
+ timestamp: new Date().toISOString()
110
+ });
111
+ }
112
+ this.addToHistory('BULK_OPERATIONS_RESULT', { result, duration });
113
+ });
114
+
115
+ // Destroy logging
116
+ baseCapability
117
+ .add("destroyStart", (event: DbPluginEvent) => {
118
+ this.logReduxAction('DESTROY_REQUEST', event.id, {
119
+ plugin: pluginName,
120
+ schemaCount: event.schemas.size
121
+ }, {
122
+ timestamp: new Date().toISOString()
123
+ });
124
+ this.addToHistory('DESTROY_REQUEST', event);
125
+ this.queryPerformance.set(event.id, now());
126
+ })
127
+ .add("destroyComplete", (result: PluginEventResultType<never>) => {
128
+ const start = this.queryPerformance.get(result.id);
129
+ this.queryPerformance.delete(result.id)
130
+ const end = now();
131
+ const duration = start == null ? -1 : end - start;
132
+ const performance = this.getPerformanceIndicator(duration);
133
+
134
+ if (result.ok === 'success') {
135
+ this.logReduxAction('DESTROY_SUCCESS', result.id, {
136
+ plugin: pluginName
137
+ }, {
138
+ duration: `${duration.toFixed(4)}ms`,
139
+ performance: performance.label,
140
+ timestamp: new Date().toISOString()
141
+ });
142
+ } else {
143
+ this.logReduxAction('DESTROY_ERROR', result.id, {
144
+ plugin: pluginName,
145
+ error: result.error?.message || result.error
146
+ }, {
147
+ duration: `${duration.toFixed(4)}ms`,
148
+ performance: performance.label,
149
+ timestamp: new Date().toISOString()
150
+ });
151
+ }
152
+ this.addToHistory('DESTROY_RESULT', { result, duration });
153
+ });
154
+
155
+ baseCapability.apply(plugin);
156
+ }
157
+
158
+ private getPerformanceIndicator(duration: number) {
159
+ if (duration > 1000) return { emoji: '🐌', color: '#ef4444', label: 'SLOW', level: 'error' };
160
+ if (duration > 500) return { emoji: '🐢', color: '#f97316', label: 'MEDIUM', level: 'warning' };
161
+ if (duration > 100) return { emoji: '⚡', color: '#eab308', label: 'FAST', level: 'info' };
162
+ return { emoji: '🚀', color: '#22c55e', label: 'INSTANT', level: 'success' };
163
+ }
164
+
165
+ private logReduxAction(action: string, eventId: string, payload: any, meta?: any) {
166
+ if (this.logStyle !== 'redux') return;
167
+
168
+ const timestamp = new Date().toISOString();
169
+
170
+ console.groupCollapsed(
171
+ `%c${action} %c@ ${timestamp}`,
172
+ 'color: #3b82f6; font-weight: bold; font-size: 14px;',
173
+ 'color: #6b7280; font-size: 12px;'
174
+ );
175
+
176
+ console.group('Action');
177
+ console.log('Type:', action);
178
+ console.log('Event ID:', eventId);
179
+ console.log('Timestamp:', timestamp);
180
+ console.groupEnd();
181
+
182
+ if (payload) {
183
+ console.group('Payload');
184
+ console.log(payload);
185
+ console.groupEnd();
186
+ }
187
+
188
+ if (meta) {
189
+ console.group('Meta');
190
+ console.log(meta);
191
+ console.groupEnd();
192
+ }
193
+
194
+ console.groupEnd();
195
+ }
196
+
197
+ private extractQueryOptions(query: any) {
198
+ const options: Record<string, any> = {};
199
+ if (query.options) {
200
+ ['skip', 'take', 'sort', 'filter', 'map', 'distinct'].forEach(type => {
201
+ try {
202
+ const values = query.options.getValues(type);
203
+ if (values.length > 0) options[type] = values;
204
+ } catch (e) {
205
+ // Skip if option type not supported
206
+ }
207
+ });
208
+ }
209
+ return options;
210
+ }
211
+
212
+ private getResultCount(result: any): string {
213
+ if (Array.isArray(result)) return `${result.length} items`;
214
+ if (result !== null && typeof result === 'object') return '1 object';
215
+ return '1 primitive';
216
+ }
217
+
218
+ private getResultType(result: any): string {
219
+ if (Array.isArray(result)) return 'array';
220
+ if (result !== null && typeof result === 'object') return 'object';
221
+ return typeof result;
222
+ }
223
+
224
+ private extractBulkOperations(operations: BulkPersistChanges) {
225
+ const aggregate = operations.aggregate;
226
+ return {
227
+ adds: aggregate.adds,
228
+ updates: aggregate.updates,
229
+ removes: aggregate.removes
230
+ };
231
+ }
232
+
233
+ private countCompletedOperations(result: BulkPersistResult | undefined): number {
234
+ return result?.aggregate.size || 0;
235
+ }
236
+
237
+ private addToHistory(type: string, data: any) {
238
+ this.logHistory.push({
239
+ type,
240
+ timestamp: Date.now(),
241
+ data
242
+ });
243
+
244
+ if (this.logHistory.length > this.maxLogEntries) {
245
+ this.logHistory = this.logHistory.slice(-this.maxLogEntries);
246
+ }
247
+ }
248
+
249
+ private generateId(): string {
250
+ return Math.random().toString(36).substr(2, 9);
251
+ }
252
+
253
+ // Public methods for debugging
254
+ getLogHistory() {
255
+ return [...this.logHistory];
256
+ }
257
+
258
+ clearLogHistory() {
259
+ this.logHistory = [];
260
+ }
261
+ }
@@ -0,0 +1 @@
1
+ export * from './DbPluginLoggingCapability';
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './translators';
3
+ export * from './capabilities';
4
+ export * from './replication';
5
+ export * from './query';
6
+ export * from './EphemeralDataPlugin';
@@ -0,0 +1,67 @@
1
+
2
+ import { CompiledSchema } from '../../schema';
3
+ import { IQuery } from '../types';
4
+ import { QueryOptionsCollection } from './QueryOptionsCollection';
5
+
6
+ export class Query<TRoot extends {}, TShape> implements IQuery<TRoot, TShape> {
7
+
8
+ readonly options: QueryOptionsCollection<TShape>;
9
+ readonly schema: CompiledSchema<TRoot>;
10
+ private enableChangeTrackingOverride?: boolean
11
+
12
+ constructor(
13
+ options: QueryOptionsCollection<TShape>,
14
+ schema: CompiledSchema<TRoot>,
15
+ enableChangeTrackingOverride?: boolean,
16
+ ) {
17
+ this.schema = schema;
18
+ this.options = options;
19
+ this.enableChangeTrackingOverride = enableChangeTrackingOverride;
20
+ }
21
+
22
+ // boolean value whether or not change tracking can be enabled on the query result
23
+ get changeTracking(): boolean {
24
+
25
+ if (this.enableChangeTrackingOverride != null) {
26
+ return this.enableChangeTrackingOverride;
27
+ }
28
+
29
+ const map = this.options.getValues("map");
30
+ const count = this.options.has("count");
31
+ const max = this.options.has("max");
32
+ const min = this.options.has("min");
33
+ const sum = this.options.has("sum");
34
+
35
+ if (map.some(x => x.fields.length > 0)) {
36
+ return false
37
+ }
38
+
39
+ if (count === true ||
40
+ max === true ||
41
+ min === true ||
42
+ sum === true) {
43
+ return false
44
+ }
45
+
46
+ return true;
47
+ }
48
+
49
+ static EMPTY<T extends {}, S>(schema: CompiledSchema<T>) {
50
+ return new Query<T, S>(QueryOptionsCollection.EMPTY<S>(), schema);
51
+ }
52
+
53
+ static isEmpty<T extends {}, S>(query: IQuery<T, S>) {
54
+ return QueryOptionsCollection.isEmpty(query.options);
55
+ }
56
+
57
+ static toString<TRoot extends {}, TShape>(query: IQuery<TRoot, TShape>) {
58
+ return JSON.stringify({
59
+ options: query.options.items,
60
+ schema: {
61
+ id: query.schema.id,
62
+ collectionName: query.schema.collectionName
63
+ },
64
+ changeTracking: query.changeTracking
65
+ })
66
+ }
67
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { QueryOptionsCollection } from './QueryOptionsCollection';
3
+ import { QueryOrdering } from './types';
4
+
5
+ describe('QueryOptionsCollection', () => {
6
+ it('should create an empty collection', () => {
7
+ const collection = QueryOptionsCollection.EMPTY();
8
+ expect(collection).toBeInstanceOf(QueryOptionsCollection);
9
+ expect(QueryOptionsCollection.isEmpty(collection)).toBe(true);
10
+ });
11
+
12
+ it('should add a skip option', () => {
13
+ const collection = QueryOptionsCollection.EMPTY();
14
+ collection.add('skip', 5);
15
+ expect(collection.has('skip')).toBe(true);
16
+ expect(collection.get('skip')).toHaveLength(1);
17
+ expect(collection.get('skip')[0].option.value).toBe(5);
18
+ });
19
+
20
+ it('should add a take option', () => {
21
+ const collection = QueryOptionsCollection.EMPTY();
22
+ collection.add('take', 10);
23
+ expect(collection.has('take')).toBe(true);
24
+ expect(collection.get('take')).toHaveLength(1);
25
+ expect(collection.get('take')[0].option.value).toBe(10);
26
+ });
27
+
28
+ it('should add a sort option', () => {
29
+ const collection = QueryOptionsCollection.EMPTY<any>();
30
+ const selector = (item: any) => item.id;
31
+ collection.add('sort', { selector, direction: QueryOrdering.Ascending, propertyName: 'id' });
32
+ expect(collection.has('sort')).toBe(true);
33
+ expect(collection.get('sort')).toHaveLength(1);
34
+ expect(collection.get('sort')[0].option.value.direction).toBe(QueryOrdering.Ascending);
35
+ });
36
+
37
+ it('should add a map option', () => {
38
+ const collection = QueryOptionsCollection.EMPTY();
39
+ const selector = (item: any) => ({ id: item.id });
40
+ collection.add('map', { selector, fields: [] });
41
+ expect(collection.has('map')).toBe(true);
42
+ expect(collection.get('map')).toHaveLength(1);
43
+ expect(collection.get('map')[0].option.value.selector).toBe(selector);
44
+ });
45
+
46
+ it('should add multiple options of the same type', () => {
47
+ const collection = QueryOptionsCollection.EMPTY();
48
+ collection.add('skip', 5);
49
+ collection.add('skip', 10);
50
+ expect(collection.get('skip')).toHaveLength(2);
51
+ expect(collection.get('skip')[0].option.value).toBe(5);
52
+ expect(collection.get('skip')[1].option.value).toBe(10);
53
+ });
54
+
55
+ it('should assign sequential indices to options', () => {
56
+ const collection = QueryOptionsCollection.EMPTY<any>();
57
+ collection.add('skip', 5);
58
+ collection.add('take', 10);
59
+ collection.add('sort', { selector: (item: any) => item.id, direction: QueryOrdering.Ascending, propertyName: 'id' });
60
+ expect(collection.get('skip')[0].index).toBe(0);
61
+ expect(collection.get('take')[0].index).toBe(1);
62
+ expect(collection.get('sort')[0].index).toBe(2);
63
+ });
64
+
65
+ it('forEach iterates options in index order across types', () => {
66
+ const collection = QueryOptionsCollection.EMPTY<any>();
67
+
68
+ collection.add('skip', 1);
69
+ collection.add('take', 2);
70
+ collection.add('sort', { selector: (i: any) => i.id, direction: QueryOrdering.Descending, propertyName: 'id' });
71
+ collection.add('skip', 3);
72
+ collection.add('take', 4);
73
+
74
+ const namesInOrder: string[] = [];
75
+ collection.forEach((opt) => {
76
+ namesInOrder.push(opt.name);
77
+ });
78
+
79
+ const expectedByIndex = [...collection.items.values()]
80
+ .flat()
81
+ .sort((a, b) => a.index - b.index)
82
+ .map(i => i.option.name);
83
+
84
+ expect(namesInOrder).toEqual(expectedByIndex);
85
+ });
86
+ });
@@ -0,0 +1,154 @@
1
+ import { forEach, isPropertyExpression } from "../../expressions/utils";
2
+ import { QueryOption, QueryOptionName, QueryOptionExecutionTarget, QueryOptionValueMap } from "./types";
3
+
4
+ export type QueryCollectionItem<T, K extends QueryOptionName> = { index: number, option: QueryOption<T, K> };
5
+
6
+ export class QueryOptionsCollection<T> {
7
+
8
+ private options: Map<QueryOptionName, QueryCollectionItem<any, any>[]> = new Map<QueryOptionName, QueryCollectionItem<any, any>[]>();
9
+ private nextExecutionTarget: QueryOptionExecutionTarget = "database";
10
+ private nextIndex: number = 0;
11
+ private enumeratedItems: QueryCollectionItem<any, any>[] = [];
12
+
13
+ get items() {
14
+ return this.options;
15
+ }
16
+
17
+ get isEmpty() {
18
+ return this.items.size === 0;
19
+ }
20
+
21
+ static EMPTY<R>() {
22
+ return new QueryOptionsCollection<R>();
23
+ }
24
+
25
+ static isEmpty<T>(options: QueryOptionsCollection<T>) {
26
+ return options.isEmpty;
27
+ }
28
+
29
+ add<K extends QueryOptionName>(name: K, value: QueryOption<T, K>["value"]) {
30
+
31
+ if (name === "map") {
32
+ const mapValue = value as QueryOptionValueMap<T>["map"];
33
+
34
+ // Evaluate the map value to see if we are renaming properties,
35
+ // if we are we need to perform everything after in memory
36
+ if (mapValue.fields.some(x => x.isRename === true) || mapValue.fields.some(x => x.property?.isUnmapped === true)) {
37
+ // Cut over to memory execution since we are renaming a property with .map
38
+ // We do not want to figure out how the new name flows through the entire query
39
+ this.nextExecutionTarget = "memory";
40
+ }
41
+ }
42
+
43
+ if (name === "filter") {
44
+ // Need to check for unmapped properties
45
+ const filterValue = value as QueryOptionValueMap<T>["filter"];
46
+
47
+ if (filterValue.expression.type === "not-parsable") {
48
+ this.nextExecutionTarget = "memory";
49
+ } else {
50
+ forEach(filterValue.expression, (expression) => {
51
+
52
+ if (isPropertyExpression(expression) && expression.property.isUnmapped) {
53
+ // Cut over to memory execution, unmapped properties are not in the database and
54
+ // cannot be queried
55
+ this.nextExecutionTarget = "memory";
56
+ return false;
57
+ }
58
+
59
+ return true;
60
+ });
61
+ }
62
+ }
63
+
64
+ const item: QueryCollectionItem<T, K> = {
65
+ index: this.nextIndex,
66
+ option: {
67
+ name,
68
+ target: this.nextExecutionTarget,
69
+ value
70
+ }
71
+ }
72
+
73
+ this.nextIndex++;
74
+
75
+ const found = this.options.get(name);
76
+
77
+ this.options.set(name, [...found ?? [], item]);
78
+ }
79
+
80
+ split(): { memory: QueryOptionsCollection<T>, database: QueryOptionsCollection<T> } {
81
+ this.resolveEnumeration();
82
+
83
+ const sortedItems = this.enumeratedItems.toSorted((a, b) => a.index - b.index);
84
+ const memoryQueryOptionsCollection = new QueryOptionsCollection<T>();
85
+ const databaseQueryOptionsCollection = new QueryOptionsCollection<T>();
86
+
87
+ for (let i = 0, length = sortedItems.length; i < length; i++) {
88
+ const sortedItem = sortedItems[i];
89
+
90
+ if (sortedItem.option.target === "database") {
91
+ databaseQueryOptionsCollection.add(sortedItem.option.name, sortedItem.option.value);
92
+ continue;
93
+ }
94
+
95
+ memoryQueryOptionsCollection.add(sortedItem.option.name, sortedItem.option.value);
96
+ }
97
+
98
+ return {
99
+ memory: memoryQueryOptionsCollection,
100
+ database: databaseQueryOptionsCollection
101
+ }
102
+ }
103
+
104
+ has<K extends QueryOptionName>(name: K): boolean {
105
+ return this.options.has(name);
106
+ }
107
+
108
+ get<K extends QueryOptionName>(name: K): QueryCollectionItem<T, K>[] {
109
+ return this.options.get(name) ?? [] as QueryCollectionItem<T, K>[];
110
+ }
111
+
112
+ getLast<K extends QueryOptionName>(name: K): QueryOption<T, K> | null {
113
+ this.resolveEnumeration();
114
+
115
+ for (let i = this.enumeratedItems.length - 1; i >= 0; i--) {
116
+ const item = this.enumeratedItems[i];
117
+
118
+ if (item.option.name === name) {
119
+ return item.option;
120
+ }
121
+ }
122
+
123
+ return null
124
+ }
125
+
126
+ getValues<K extends QueryOptionName>(name: K): QueryCollectionItem<T, K>["option"]["value"][] | undefined {
127
+
128
+ const found = this.options.get(name);
129
+
130
+ if (found == null) {
131
+ return [];
132
+ }
133
+
134
+ return found.map(w => w.option.value) as QueryCollectionItem<T, K>["option"]["value"][];
135
+ }
136
+
137
+ private getEnumeration() {
138
+ return [...this.options.values()].flat().toSorted((a, b) => a.index - b.index);
139
+ }
140
+
141
+ private resolveEnumeration() {
142
+ if (this.enumeratedItems.length != this.nextIndex) {
143
+ this.enumeratedItems = this.getEnumeration();
144
+ }
145
+ }
146
+
147
+ forEach(iterator: (item: QueryCollectionItem<T, any>["option"]) => void) {
148
+ this.resolveEnumeration();
149
+
150
+ for (let i = 0, length = this.enumeratedItems.length; i < length; i++) {
151
+ iterator(this.enumeratedItems[i].option);
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Query';
2
+ export * from './QueryOptionsCollection';
3
+ export * from './types';
@@ -0,0 +1,46 @@
1
+ import { Expression, Filter, ParamsFilter } from "../../expressions";
2
+ import { PropertyInfo } from "../../schema";
3
+ import { GenericFunction } from "../../types";
4
+
5
+ export enum QueryOrdering {
6
+ Descending = "desc",
7
+ Ascending = "asc"
8
+ }
9
+
10
+ /**
11
+ * Field mapping for a query result, including source and destination names and a getter function.
12
+ */
13
+ export type QueryField = {
14
+ sourceName: string,
15
+ destinationName: string,
16
+ isRename: boolean;
17
+ property?: PropertyInfo<unknown>;
18
+ getter: <T>(data: Record<string, unknown>) => T;
19
+ };
20
+
21
+ export type QueryOptionExecutionTarget = "database" | "memory";
22
+ export type QueryOptionName = keyof QueryOptionValueMap<unknown>;
23
+
24
+ export type QueryOption<T, K extends QueryOptionName> = {
25
+ name: QueryOptionName;
26
+ value: QueryOptionValueMap<T>[K],
27
+ target: QueryOptionExecutionTarget;
28
+ }
29
+
30
+ export type QueryOptionValueMap<T extends {}> = {
31
+ skip: number;
32
+ take: number;
33
+ sort: { selector: GenericFunction<T, T[keyof T]>, direction: QueryOrdering, propertyName: string };
34
+ map: { selector: GenericFunction<T, any>, fields: QueryField[] };
35
+ filter: { params?: {}, filter: ParamsFilter<T, {}> | Filter<T>, expression: Expression };
36
+ min: true; // True or not set
37
+ max: true; // True or not set
38
+ count: true; // True or not set
39
+ sum: true; // True or not set
40
+ distinct: true; // True or not set
41
+ };
42
+
43
+ /**
44
+ * Sort specification for a query.
45
+ */
46
+ export type QuerySort = { key: string, selector: (item: unknown) => unknown, direction: "asc" | "desc" };