@stonyx/orm 0.2.1-alpha.15 → 0.2.1-alpha.17

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/src/store.js CHANGED
@@ -1,5 +1,6 @@
1
- import { relationships } from '@stonyx/orm';
1
+ import Orm, { relationships } from '@stonyx/orm';
2
2
  import { TYPES } from './relationships.js';
3
+ import ViewResolver from './view-resolver.js';
3
4
 
4
5
  export default class Store {
5
6
  constructor() {
@@ -28,6 +29,12 @@ export default class Store {
28
29
  * @returns {Promise<Record|undefined>}
29
30
  */
30
31
  async find(modelName, id) {
32
+ // For views in non-MySQL mode, use view resolver
33
+ if (Orm.instance?.isView?.(modelName) && !this._mysqlDb) {
34
+ const resolver = new ViewResolver(modelName);
35
+ return resolver.resolveOne(id);
36
+ }
37
+
31
38
  // For memory: true models, the store is authoritative
32
39
  if (this._isMemoryModel(modelName)) {
33
40
  return this.get(modelName, id);
@@ -50,6 +57,18 @@ export default class Store {
50
57
  * @returns {Promise<Record[]>}
51
58
  */
52
59
  async findAll(modelName, conditions) {
60
+ // For views in non-MySQL mode, use view resolver
61
+ if (Orm.instance?.isView?.(modelName) && !this._mysqlDb) {
62
+ const resolver = new ViewResolver(modelName);
63
+ const records = await resolver.resolveAll();
64
+
65
+ if (!conditions || Object.keys(conditions).length === 0) return records;
66
+
67
+ return records.filter(record =>
68
+ Object.entries(conditions).every(([key, value]) => record.__data[key] === value)
69
+ );
70
+ }
71
+
53
72
  // For memory: true models without conditions, return from store
54
73
  if (this._isMemoryModel(modelName) && !conditions) {
55
74
  const modelStore = this.get(modelName);
@@ -125,6 +144,11 @@ export default class Store {
125
144
  }
126
145
 
127
146
  remove(key, id) {
147
+ // Guard: read-only views cannot have records removed
148
+ if (Orm.instance?.isView?.(key)) {
149
+ throw new Error(`Cannot remove records from read-only view '${key}'`);
150
+ }
151
+
128
152
  if (id) return this.unloadRecord(key, id);
129
153
 
130
154
  this.unloadAllRecords(key);
@@ -0,0 +1,183 @@
1
+ import Orm, { createRecord, store } from '@stonyx/orm';
2
+ import { AggregateProperty } from './aggregates.js';
3
+ import { get } from '@stonyx/utils/object';
4
+
5
+ export default class ViewResolver {
6
+ constructor(viewName) {
7
+ this.viewName = viewName;
8
+ }
9
+
10
+ async resolveAll() {
11
+ const orm = Orm.instance;
12
+ const { modelClass: viewClass } = orm.getRecordClasses(this.viewName);
13
+
14
+ if (!viewClass) return [];
15
+
16
+ const source = viewClass.source;
17
+ if (!source) return [];
18
+
19
+ const sourceRecords = await store.findAll(source);
20
+ if (!sourceRecords || sourceRecords.length === 0) {
21
+ return [];
22
+ }
23
+
24
+ const resolveMap = viewClass.resolve || {};
25
+ const viewInstance = new viewClass(this.viewName);
26
+ const aggregateFields = {};
27
+ const regularFields = {};
28
+
29
+ // Categorize fields on the view instance
30
+ for (const [key, value] of Object.entries(viewInstance)) {
31
+ if (key.startsWith('__')) continue;
32
+ if (key === 'id') continue;
33
+
34
+ if (value instanceof AggregateProperty) {
35
+ aggregateFields[key] = value;
36
+ } else if (typeof value !== 'function') {
37
+ // Regular attr or direct value — not a relationship handler
38
+ regularFields[key] = value;
39
+ }
40
+ }
41
+
42
+ const groupByField = viewClass.groupBy;
43
+
44
+ if (groupByField) {
45
+ return this._resolveGroupBy(sourceRecords, groupByField, aggregateFields, regularFields, resolveMap, viewClass);
46
+ }
47
+
48
+ return this._resolvePerRecord(sourceRecords, aggregateFields, regularFields, resolveMap, viewClass);
49
+ }
50
+
51
+ _resolvePerRecord(sourceRecords, aggregateFields, regularFields, resolveMap, viewClass) {
52
+ const results = [];
53
+
54
+ for (const sourceRecord of sourceRecords) {
55
+ const rawData = { id: sourceRecord.id };
56
+
57
+ // Compute aggregate fields from source record's relationships
58
+ for (const [key, aggProp] of Object.entries(aggregateFields)) {
59
+ const relatedRecords = sourceRecord.__relationships?.[aggProp.relationship]
60
+ || sourceRecord[aggProp.relationship];
61
+ const relArray = Array.isArray(relatedRecords) ? relatedRecords : [];
62
+ rawData[key] = aggProp.compute(relArray);
63
+ }
64
+
65
+ // Apply resolve map entries
66
+ for (const [key, resolver] of Object.entries(resolveMap)) {
67
+ if (typeof resolver === 'function') {
68
+ rawData[key] = resolver(sourceRecord);
69
+ } else if (typeof resolver === 'string') {
70
+ rawData[key] = get(sourceRecord.__data || sourceRecord, resolver)
71
+ ?? get(sourceRecord, resolver);
72
+ }
73
+ }
74
+
75
+ // Map regular attr fields from source record if not already set
76
+ for (const key of Object.keys(regularFields)) {
77
+ if (rawData[key] !== undefined) continue;
78
+
79
+ const sourceValue = sourceRecord.__data?.[key] ?? sourceRecord[key];
80
+ if (sourceValue !== undefined) {
81
+ rawData[key] = sourceValue;
82
+ }
83
+ }
84
+
85
+ // Set belongsTo source relationship
86
+ const viewInstanceForRel = new viewClass(this.viewName);
87
+ for (const [key, value] of Object.entries(viewInstanceForRel)) {
88
+ if (typeof value === 'function' && key !== 'id') {
89
+ // This is a relationship handler — pass the source record id
90
+ rawData[key] = sourceRecord.id;
91
+ }
92
+ }
93
+
94
+ // Clear existing record from store to allow re-resolution
95
+ const viewStore = store.get(this.viewName);
96
+ if (viewStore?.has(rawData.id)) {
97
+ viewStore.delete(rawData.id);
98
+ }
99
+
100
+ const record = createRecord(this.viewName, rawData, { isDbRecord: true });
101
+ results.push(record);
102
+ }
103
+
104
+ return results;
105
+ }
106
+
107
+ _resolveGroupBy(sourceRecords, groupByField, aggregateFields, regularFields, resolveMap, viewClass) {
108
+ // Group source records by the groupBy field value
109
+ const groups = new Map();
110
+ for (const record of sourceRecords) {
111
+ const key = record.__data?.[groupByField] ?? record[groupByField];
112
+ if (!groups.has(key)) {
113
+ groups.set(key, []);
114
+ }
115
+ groups.get(key).push(record);
116
+ }
117
+
118
+ const results = [];
119
+
120
+ for (const [groupKey, groupRecords] of groups) {
121
+ const rawData = { id: groupKey };
122
+
123
+ // Compute aggregate fields
124
+ for (const [key, aggProp] of Object.entries(aggregateFields)) {
125
+ if (aggProp.relationship === undefined) {
126
+ // Field-level aggregate — compute over group records directly
127
+ rawData[key] = aggProp.compute(groupRecords);
128
+ } else {
129
+ // Relationship aggregate — flatten related records across all group members
130
+ const allRelated = [];
131
+ for (const record of groupRecords) {
132
+ const relatedRecords = record.__relationships?.[aggProp.relationship]
133
+ || record[aggProp.relationship];
134
+ if (Array.isArray(relatedRecords)) {
135
+ allRelated.push(...relatedRecords);
136
+ }
137
+ }
138
+ rawData[key] = aggProp.compute(allRelated);
139
+ }
140
+ }
141
+
142
+ // Apply resolve map entries — functions receive the group array
143
+ for (const [key, resolver] of Object.entries(resolveMap)) {
144
+ if (typeof resolver === 'function') {
145
+ rawData[key] = resolver(groupRecords);
146
+ } else if (typeof resolver === 'string') {
147
+ // String path — take value from first record in group
148
+ const first = groupRecords[0];
149
+ rawData[key] = get(first.__data || first, resolver)
150
+ ?? get(first, resolver);
151
+ }
152
+ }
153
+
154
+ // Map regular attr fields from first record if not already set
155
+ for (const key of Object.keys(regularFields)) {
156
+ if (rawData[key] !== undefined) continue;
157
+ const first = groupRecords[0];
158
+ const sourceValue = first.__data?.[key] ?? first[key];
159
+ if (sourceValue !== undefined) {
160
+ rawData[key] = sourceValue;
161
+ }
162
+ }
163
+
164
+ // Clear existing record from store to allow re-resolution
165
+ const viewStore = store.get(this.viewName);
166
+ if (viewStore?.has(rawData.id)) {
167
+ viewStore.delete(rawData.id);
168
+ }
169
+
170
+ const record = createRecord(this.viewName, rawData, { isDbRecord: true });
171
+ results.push(record);
172
+ }
173
+
174
+ return results;
175
+ }
176
+
177
+ async resolveOne(id) {
178
+ const all = await this.resolveAll();
179
+ return all.find(record => {
180
+ return record.id === id || record.id == id;
181
+ });
182
+ }
183
+ }
package/src/view.js ADDED
@@ -0,0 +1,21 @@
1
+ import { attr } from '@stonyx/orm';
2
+
3
+ export default class View {
4
+ static memory = false;
5
+ static readOnly = true;
6
+ static pluralName = undefined;
7
+ static source = undefined;
8
+ static groupBy = undefined;
9
+ static resolve = undefined;
10
+
11
+ id = attr('number');
12
+
13
+ constructor(name) {
14
+ this.__name = name;
15
+
16
+ // Enforce readOnly — cannot be overridden to false
17
+ if (this.constructor.readOnly !== true) {
18
+ throw new Error(`View '${name}' cannot override readOnly to false`);
19
+ }
20
+ }
21
+ }