@warp-drive/core 5.7.0-alpha.0 → 5.7.0-alpha.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.
- package/declarations/reactive/-private/default-mode.d.ts +73 -0
- package/declarations/reactive/-private/fields/get-field-key.d.ts +8 -0
- package/declarations/reactive/-private/fields/managed-array.d.ts +3 -5
- package/declarations/reactive/-private/fields/managed-object.d.ts +5 -3
- package/declarations/reactive/-private/kind/alias-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/array-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/attribute-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/belongs-to-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/collection-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/derived-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/generic-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/has-many-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/hash-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/identity-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/local-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/object-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/resource-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/schema-array-field.d.ts +4 -0
- package/declarations/reactive/-private/kind/schema-object-field.d.ts +4 -0
- package/declarations/reactive/-private/record.d.ts +2 -4
- package/declarations/reactive/-private/schema.d.ts +47 -3
- package/declarations/reactive.d.ts +1 -0
- package/declarations/request/-private/fetch.d.ts +2 -0
- package/declarations/store/-private/caches/instance-cache.d.ts +1 -8
- package/declarations/store/-private/store-service.d.ts +17 -777
- package/declarations/store/-private.d.ts +3 -2
- package/declarations/store/-types/q/schema-service.d.ts +56 -32
- package/declarations/store/-types/q/store.d.ts +6 -7
- package/declarations/store/deprecated/-private.d.ts +235 -0
- package/declarations/store/deprecated/store.d.ts +788 -0
- package/declarations/types/cache.d.ts +0 -2
- package/declarations/types/params.d.ts +2 -3
- package/declarations/types/request.d.ts +6 -6
- package/declarations/types/schema/fields.d.ts +361 -11
- package/declarations/types.d.ts +2 -1
- package/dist/graph/-private.js +1 -1
- package/dist/{handler-C2T-IyJK.js → handler-SdXlte1w.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/reactive.js +1287 -589
- package/dist/{request-state-CjLph1LP.js → request-state-CeN66aML.js} +851 -1346
- package/dist/store/-private.js +2 -2
- package/dist/types/-private.js +1 -1
- package/dist/types/schema/fields.js +17 -2
- package/package.json +3 -3
- package/declarations/reactive/-private/fields/compute.d.ts +0 -43
- package/declarations/store/-private/legacy-model-support/record-reference.d.ts +0 -159
- package/declarations/store/-private/legacy-model-support/shim-model-class.d.ts +0 -17
- package/declarations/store/-types/q/ds-model.d.ts +0 -21
package/dist/reactive.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { isResourceSchema } from './types/schema/fields.js';
|
|
2
|
-
import {
|
|
2
|
+
import { F as withSignalStore, T as isExtensionProp, U as performExtensionSet, H as consumeInternalSignal, V as performArrayExtensionGet, x as entangleSignal, E as peekInternalSignal, d as SOURCE$1, f as fastPush, l as RelatedCollection, J as getOrCreateInternalSignal, G as notifyInternalSignal, W as performObjectExtensionGet, y as defineSignal, B as Signals, h as setRecordIdentifier, r as recordIdentifierFor } from "./request-state-CeN66aML.js";
|
|
3
3
|
import { EnableHydration, STRUCTURED } from './types/request.js';
|
|
4
4
|
import { macroCondition, getGlobalConfig } from '@embroider/macros';
|
|
5
|
-
import { deprecate } from '@ember/debug';
|
|
5
|
+
import { warn, deprecate } from '@ember/debug';
|
|
6
6
|
import './utils/string.js';
|
|
7
7
|
import { A as ARRAY_SIGNAL, O as OBJECT_SIGNAL, c as createMemo } from "./configure-B48bFHOl.js";
|
|
8
8
|
import { RecordStore, Type } from './types/symbols.js';
|
|
9
|
+
import { S as SOURCE, E as Editable, L as Legacy, D as Destroy, I as Identifier, P as Parent, a as EmbeddedPath, C as Checkout, b as EmbeddedField } from "./symbols-SIstXMLI.js";
|
|
9
10
|
import { getOrSetGlobal } from './types/-private.js';
|
|
10
|
-
import { S as SOURCE, E as Editable, L as Legacy, I as Identifier, P as Parent, a as EmbeddedPath, D as Destroy, C as Checkout, b as EmbeddedField } from "./symbols-SIstXMLI.js";
|
|
11
11
|
import './index.js';
|
|
12
|
+
function getAliasField(context) {
|
|
13
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
14
|
+
{
|
|
15
|
+
throw new Error(`Alias field access is not implemented`);
|
|
16
|
+
}
|
|
17
|
+
})() : {};
|
|
18
|
+
}
|
|
19
|
+
function setAliasField(context) {
|
|
20
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
21
|
+
{
|
|
22
|
+
throw new Error(`Alias field setting is not implemented`);
|
|
23
|
+
}
|
|
24
|
+
})() : {};
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
12
27
|
const ARRAY_GETTER_METHODS = new Set([Symbol.iterator, 'concat', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'reduce', 'reduceRight', 'slice', 'some', 'values']);
|
|
13
28
|
// const ARRAY_SETTER_METHODS = new Set<KeyType>(['push', 'pop', 'unshift', 'shift', 'splice', 'sort']);
|
|
14
29
|
const SYNC_PROPS = new Set(['[]', 'length']);
|
|
@@ -55,21 +70,27 @@ function safeForEach(instance, arr, store, callback, target) {
|
|
|
55
70
|
}
|
|
56
71
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
57
72
|
class ManagedArray {
|
|
58
|
-
constructor(
|
|
73
|
+
constructor(context, owner, data) {
|
|
59
74
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
60
75
|
const self = this;
|
|
61
76
|
this[SOURCE] = data?.slice();
|
|
62
|
-
const IS_EDITABLE = this[Editable] = editable ?? false;
|
|
63
|
-
this[Legacy] = legacy;
|
|
77
|
+
const IS_EDITABLE = this[Editable] = context.editable ?? false;
|
|
78
|
+
this[Legacy] = context.legacy;
|
|
79
|
+
const schema = context.store.schema;
|
|
80
|
+
const cache = context.store.cache;
|
|
81
|
+
const {
|
|
82
|
+
field
|
|
83
|
+
} = context;
|
|
64
84
|
const signals = withSignalStore(this);
|
|
65
85
|
let _SIGNAL = null;
|
|
66
86
|
const boundFns = new Map();
|
|
67
|
-
this.identifier =
|
|
68
|
-
this.path = path;
|
|
87
|
+
this.identifier = context.resourceKey;
|
|
88
|
+
this.path = context.path;
|
|
69
89
|
this.owner = owner;
|
|
70
90
|
let transaction = false;
|
|
71
|
-
const
|
|
72
|
-
|
|
91
|
+
const KeyMode = field.options?.key ?? '@identity';
|
|
92
|
+
// listener.
|
|
93
|
+
const RefStorage = KeyMode === '@identity' ? WeakMap :
|
|
73
94
|
// CAUTION CAUTION CAUTION
|
|
74
95
|
// this is a pile of lies
|
|
75
96
|
// the Map is Map<string, WeakRef<ReactiveResource>>
|
|
@@ -77,8 +98,8 @@ class ManagedArray {
|
|
|
77
98
|
// internal to a method like ours without us duplicating the code
|
|
78
99
|
// into two separate methods.
|
|
79
100
|
Map;
|
|
80
|
-
const ManagedRecordRefs =
|
|
81
|
-
const extensions = legacy ? schema.CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) : null;
|
|
101
|
+
const ManagedRecordRefs = field.kind === 'schema-array' ? new RefStorage() : null;
|
|
102
|
+
const extensions = context.legacy ? schema.CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) : null;
|
|
82
103
|
const proxy = new Proxy(this[SOURCE], {
|
|
83
104
|
get(target, prop, receiver) {
|
|
84
105
|
if (prop === ARRAY_SIGNAL) {
|
|
@@ -93,7 +114,7 @@ class ManagedArray {
|
|
|
93
114
|
const index = convertToInt(prop);
|
|
94
115
|
if (_SIGNAL.isStale && (index !== null || SYNC_PROPS.has(prop) || isArrayGetter(prop))) {
|
|
95
116
|
_SIGNAL.isStale = false;
|
|
96
|
-
const newData = cache.getAttr(
|
|
117
|
+
const newData = cache.getAttr(context.resourceKey, context.path);
|
|
97
118
|
if (newData && newData !== self[SOURCE]) {
|
|
98
119
|
self[SOURCE].length = 0;
|
|
99
120
|
self[SOURCE].push(...newData);
|
|
@@ -104,14 +125,72 @@ class ManagedArray {
|
|
|
104
125
|
}
|
|
105
126
|
if (prop === '[]') return consumeInternalSignal(_SIGNAL), receiver;
|
|
106
127
|
if (index !== null) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
if (!transaction) {
|
|
129
|
+
consumeInternalSignal(_SIGNAL);
|
|
130
|
+
}
|
|
131
|
+
const rawValue = target[index];
|
|
132
|
+
if (field.kind === 'array') {
|
|
133
|
+
if (field.type) {
|
|
134
|
+
const transform = schema.transformation(field);
|
|
135
|
+
return transform.hydrate(rawValue, field.options ?? null, self.owner);
|
|
136
|
+
}
|
|
137
|
+
return rawValue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* When the array is polymorphic, we need to determine the real type
|
|
142
|
+
* in order to apply the correct identity as schema-object identity
|
|
143
|
+
* is only required to be unique by type
|
|
144
|
+
*/
|
|
145
|
+
let objectType;
|
|
146
|
+
if (field.options?.polymorphic) {
|
|
147
|
+
const typePath = field.options.type ?? 'type';
|
|
148
|
+
// if we are polymorphic, then context.field.options.type will
|
|
149
|
+
// either specify a path on the rawValue to use as the type, defaulting to "type" or
|
|
150
|
+
// the special string "@hash" which tells us to treat field.type as a hashFn name with which
|
|
151
|
+
// to calc the type.
|
|
152
|
+
if (typePath === '@hash') {
|
|
153
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
154
|
+
if (!test) {
|
|
155
|
+
throw new Error(`Expected the field to define a hashFn as its type`);
|
|
156
|
+
}
|
|
157
|
+
})(field.type) : {};
|
|
158
|
+
const hashFn = schema.hashFn({
|
|
159
|
+
type: field.type
|
|
160
|
+
});
|
|
161
|
+
// TODO consider if there are better options and name args we could provide.
|
|
162
|
+
objectType = hashFn(rawValue, null, null);
|
|
163
|
+
} else {
|
|
164
|
+
objectType = rawValue[typePath];
|
|
165
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
166
|
+
if (!test) {
|
|
167
|
+
throw new Error(`Expected the type path for the field to be a value on the raw object`);
|
|
168
|
+
}
|
|
169
|
+
})(typePath && objectType && typeof objectType === 'string') : {};
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
173
|
+
if (!test) {
|
|
174
|
+
throw new Error(`A non-polymorphic SchemaArrayField must provide a SchemaObject type in its definition`);
|
|
175
|
+
}
|
|
176
|
+
})(field.type) : {};
|
|
177
|
+
objectType = field.type;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* When KeyMode=@hash the ReactiveResource is keyed into
|
|
182
|
+
* ManagedRecordRefs by the return value of @hash on the rawValue.
|
|
183
|
+
*
|
|
184
|
+
* This means that we could find a way to only recompute the identity
|
|
185
|
+
* when ARRAY_SIGNAL is dirty if hash performance becomes a bottleneck.
|
|
186
|
+
*/
|
|
187
|
+
let schemaObjectKeyValue;
|
|
188
|
+
if (KeyMode === '@hash') {
|
|
110
189
|
const hashField = schema.resource({
|
|
111
|
-
type:
|
|
190
|
+
type: objectType
|
|
112
191
|
}).identity;
|
|
113
192
|
const hashFn = schema.hashFn(hashField);
|
|
114
|
-
|
|
193
|
+
schemaObjectKeyValue = hashFn(rawValue, hashField.options ?? null, hashField.name);
|
|
115
194
|
} else {
|
|
116
195
|
// if mode is not @identity or @index, then access the key path.
|
|
117
196
|
// we should assert that `mode` is a string
|
|
@@ -119,69 +198,78 @@ class ManagedArray {
|
|
|
119
198
|
// and, we likely should lookup the associated field and throw an error IF
|
|
120
199
|
// the given field does not exist OR
|
|
121
200
|
// the field is anything other than a GenericField or LegacyAttributeField.
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
if (isSchemaArray) {
|
|
145
|
-
if (!transaction) {
|
|
146
|
-
consumeInternalSignal(_SIGNAL);
|
|
147
|
-
}
|
|
148
|
-
if (val) {
|
|
149
|
-
const recordRef = ManagedRecordRefs.get(val);
|
|
150
|
-
let record = recordRef?.deref();
|
|
151
|
-
if (!record) {
|
|
152
|
-
const recordPath = path.slice();
|
|
153
|
-
// this is a dirty lie since path is string[] but really we
|
|
154
|
-
// should change the types for paths to `Array<string | number>`
|
|
155
|
-
// TODO we should allow the schema for the field to define a "key"
|
|
156
|
-
// for stability. Default should be `@identity` which means that
|
|
157
|
-
// same object reference from cache should result in same ReactiveResource
|
|
158
|
-
// embedded object.
|
|
159
|
-
recordPath.push(index);
|
|
160
|
-
const recordIdentifier = self.owner[Identifier] || self.owner[Parent];
|
|
161
|
-
record = new ReactiveResource(store, recordIdentifier, {
|
|
162
|
-
[Editable]: self.owner[Editable],
|
|
163
|
-
[Legacy]: self.owner[Legacy]
|
|
164
|
-
}, true, field, recordPath);
|
|
165
|
-
// if mode is not @identity or @index, then access the key path now
|
|
166
|
-
// to determine the key value.
|
|
167
|
-
// chris says we can implement this as a special kind `@hash` which
|
|
168
|
-
// would be a function that only has access to the cache value and not
|
|
169
|
-
// the record itself, so derivation is possible but intentionally limited
|
|
170
|
-
// and non-reactive?
|
|
171
|
-
ManagedRecordRefs.set(val, new WeakRef(record));
|
|
201
|
+
if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
|
|
202
|
+
const isPathKeyMode = KeyMode !== '@identity' && KeyMode !== '@index';
|
|
203
|
+
if (isPathKeyMode) {
|
|
204
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
205
|
+
if (!test) {
|
|
206
|
+
throw new Error('mode must be a string');
|
|
207
|
+
}
|
|
208
|
+
})(typeof KeyMode === 'string' && KeyMode !== '') : {};
|
|
209
|
+
const modeField = schema.fields({
|
|
210
|
+
type: objectType
|
|
211
|
+
}).get(KeyMode);
|
|
212
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
213
|
+
if (!test) {
|
|
214
|
+
throw new Error('field must exist in schema');
|
|
215
|
+
}
|
|
216
|
+
})(modeField) : {};
|
|
217
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
218
|
+
if (!test) {
|
|
219
|
+
throw new Error('field must be a GenericField or LegacyAttributeField');
|
|
220
|
+
}
|
|
221
|
+
})(modeField.kind === 'field' || modeField.kind === 'attribute') : {};
|
|
172
222
|
}
|
|
173
|
-
return record;
|
|
174
223
|
}
|
|
175
|
-
|
|
224
|
+
schemaObjectKeyValue = KeyMode === '@identity' ? rawValue : KeyMode === '@index' ? index : rawValue[KeyMode];
|
|
176
225
|
}
|
|
177
|
-
if (!
|
|
178
|
-
|
|
226
|
+
if (!schemaObjectKeyValue) {
|
|
227
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
228
|
+
{
|
|
229
|
+
throw new Error(`Unexpected out of bounds access on SchemaArray`);
|
|
230
|
+
}
|
|
231
|
+
})() : {};
|
|
232
|
+
return undefined;
|
|
179
233
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
234
|
+
const recordRef = ManagedRecordRefs.get(schemaObjectKeyValue);
|
|
235
|
+
const record = recordRef?.value.deref();
|
|
236
|
+
|
|
237
|
+
// confirm the type and key still match
|
|
238
|
+
if (record && recordRef.type === objectType && recordRef.identity === schemaObjectKeyValue) {
|
|
239
|
+
if (recordRef.index !== index) {
|
|
240
|
+
recordRef.index = index;
|
|
241
|
+
recordRef.context.path[recordRef.context.path.length - 1] = index;
|
|
242
|
+
}
|
|
243
|
+
return record;
|
|
244
|
+
} else if (record) {
|
|
245
|
+
// TODO schedule idle once we can
|
|
246
|
+
void Promise.resolve().then(() => {
|
|
247
|
+
record[Destroy]();
|
|
248
|
+
});
|
|
183
249
|
}
|
|
184
|
-
|
|
250
|
+
const recordPath = context.path.slice();
|
|
251
|
+
// this is a dirty lie since path is string[] but really we
|
|
252
|
+
// should change the types for paths to `Array<string | number>`
|
|
253
|
+
recordPath.push(index);
|
|
254
|
+
const objectContext = {
|
|
255
|
+
store: context.store,
|
|
256
|
+
resourceKey: context.resourceKey,
|
|
257
|
+
modeName: context.modeName,
|
|
258
|
+
legacy: context.legacy,
|
|
259
|
+
editable: context.editable,
|
|
260
|
+
path: recordPath,
|
|
261
|
+
field: field,
|
|
262
|
+
value: objectType
|
|
263
|
+
};
|
|
264
|
+
const schemaObject = new ReactiveResource(objectContext);
|
|
265
|
+
ManagedRecordRefs.set(schemaObjectKeyValue, {
|
|
266
|
+
type: objectType,
|
|
267
|
+
identity: schemaObjectKeyValue,
|
|
268
|
+
index,
|
|
269
|
+
context: objectContext,
|
|
270
|
+
value: new WeakRef(schemaObject)
|
|
271
|
+
});
|
|
272
|
+
return schemaObject;
|
|
185
273
|
}
|
|
186
274
|
if (isArrayGetter(prop)) {
|
|
187
275
|
let fn = boundFns.get(prop);
|
|
@@ -190,7 +278,7 @@ class ManagedArray {
|
|
|
190
278
|
fn = function () {
|
|
191
279
|
consumeInternalSignal(_SIGNAL);
|
|
192
280
|
transaction = true;
|
|
193
|
-
const result = safeForEach(receiver, target, store, arguments[0], arguments[1]);
|
|
281
|
+
const result = safeForEach(receiver, target, context.store, arguments[0], arguments[1]);
|
|
194
282
|
transaction = false;
|
|
195
283
|
return result;
|
|
196
284
|
};
|
|
@@ -233,9 +321,9 @@ class ManagedArray {
|
|
|
233
321
|
},
|
|
234
322
|
set(target, prop, value, receiver) {
|
|
235
323
|
if (!IS_EDITABLE) {
|
|
236
|
-
let errorPath =
|
|
237
|
-
if (path) {
|
|
238
|
-
errorPath = path[path.length - 1];
|
|
324
|
+
let errorPath = context.resourceKey.type;
|
|
325
|
+
if (context.path) {
|
|
326
|
+
errorPath = context.path[context.path.length - 1];
|
|
239
327
|
}
|
|
240
328
|
throw new Error(`Cannot set ${String(prop)} on ${errorPath} because the record is not editable`);
|
|
241
329
|
}
|
|
@@ -253,19 +341,19 @@ class ManagedArray {
|
|
|
253
341
|
const reflect = Reflect.set(target, prop, value, receiver);
|
|
254
342
|
if (reflect) {
|
|
255
343
|
if (!field.type) {
|
|
256
|
-
cache.setAttr(
|
|
344
|
+
cache.setAttr(context.resourceKey, context.path, self[SOURCE]);
|
|
257
345
|
_SIGNAL.isStale = true;
|
|
258
346
|
return true;
|
|
259
347
|
}
|
|
260
348
|
let rawValue = self[SOURCE];
|
|
261
|
-
if (
|
|
349
|
+
if (field.kind !== 'schema-array') {
|
|
262
350
|
const transform = schema.transformation(field);
|
|
263
351
|
if (!transform) {
|
|
264
|
-
throw new Error(`No '${field.type}' transform defined for use by ${
|
|
352
|
+
throw new Error(`No '${field.type}' transform defined for use by ${context.resourceKey.type}.${String(prop)}`);
|
|
265
353
|
}
|
|
266
354
|
rawValue = self[SOURCE].map(item => transform.serialize(item, field.options ?? null, self.owner));
|
|
267
355
|
}
|
|
268
|
-
cache.setAttr(
|
|
356
|
+
cache.setAttr(context.resourceKey, context.path, rawValue);
|
|
269
357
|
_SIGNAL.isStale = true;
|
|
270
358
|
}
|
|
271
359
|
return reflect;
|
|
@@ -302,27 +390,453 @@ const desc = {
|
|
|
302
390
|
};
|
|
303
391
|
// compat(desc);
|
|
304
392
|
Object.defineProperty(ManagedArray.prototype, '[]', desc);
|
|
393
|
+
function getArrayField(context) {
|
|
394
|
+
const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
395
|
+
// the thing we hand out needs to know its owner and path in a private manner
|
|
396
|
+
// its "address" is the parent identifier (identifier) + field name (field.name)
|
|
397
|
+
// in the nested object case field name here is the full dot path from root resource to this value
|
|
398
|
+
// its "key" is the field on the parent record
|
|
399
|
+
// its "owner" is the parent record
|
|
400
|
+
const {
|
|
401
|
+
record
|
|
402
|
+
} = context;
|
|
403
|
+
let managedArray = signal.value;
|
|
404
|
+
if (managedArray) {
|
|
405
|
+
return managedArray;
|
|
406
|
+
} else {
|
|
407
|
+
const {
|
|
408
|
+
store,
|
|
409
|
+
resourceKey,
|
|
410
|
+
path
|
|
411
|
+
} = context;
|
|
412
|
+
const {
|
|
413
|
+
cache
|
|
414
|
+
} = store;
|
|
415
|
+
const rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
|
|
416
|
+
if (!rawValue) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
managedArray = new ManagedArray(context, record, rawValue);
|
|
420
|
+
signal.value = managedArray;
|
|
421
|
+
}
|
|
422
|
+
return managedArray;
|
|
423
|
+
}
|
|
424
|
+
function setArrayField(context) {
|
|
425
|
+
const {
|
|
426
|
+
field,
|
|
427
|
+
record,
|
|
428
|
+
value
|
|
429
|
+
} = context;
|
|
430
|
+
const {
|
|
431
|
+
cache,
|
|
432
|
+
schema
|
|
433
|
+
} = context.store;
|
|
434
|
+
const fieldSignal = peekInternalSignal(context.signals, context.path.at(-1));
|
|
435
|
+
const peeked = fieldSignal?.value;
|
|
436
|
+
const transform = field.type ? schema.transformation(field) : null;
|
|
437
|
+
const rawValue = field.type ? value.map(item => transform.serialize(item, field.options ?? null, record)) : value?.slice();
|
|
438
|
+
cache.setAttr(context.resourceKey, context.path, rawValue);
|
|
439
|
+
if (peeked) {
|
|
440
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
441
|
+
if (!test) {
|
|
442
|
+
throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
|
|
443
|
+
}
|
|
444
|
+
})(ARRAY_SIGNAL in peeked) : {};
|
|
445
|
+
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
446
|
+
arrSignal.isStale = true;
|
|
447
|
+
// TODO run array destroy?
|
|
448
|
+
}
|
|
449
|
+
if (!Array.isArray(rawValue) && fieldSignal) {
|
|
450
|
+
fieldSignal.value = null;
|
|
451
|
+
}
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
function getAttributeField(context) {
|
|
455
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
456
|
+
const {
|
|
457
|
+
cache
|
|
458
|
+
} = context.store;
|
|
459
|
+
return context.editable ? cache.getAttr(context.resourceKey, context.path) : cache.getRemoteAttr(context.resourceKey, context.path);
|
|
460
|
+
}
|
|
461
|
+
function setAttributeField(context) {
|
|
462
|
+
context.store.cache.setAttr(context.resourceKey, context.path, context.value);
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
const InvalidKinds = ['alias', 'derived', '@local'];
|
|
466
|
+
function isInvalidKind(kind) {
|
|
467
|
+
return InvalidKinds.includes(kind);
|
|
468
|
+
}
|
|
469
|
+
function isNonIdentityCacheableField(field) {
|
|
470
|
+
return !isInvalidKind(field.kind) && field.kind !== '@id' && field.kind !== '@hash';
|
|
471
|
+
}
|
|
472
|
+
function getFieldCacheKeyStrict(field) {
|
|
473
|
+
return field.sourceKey || field.name;
|
|
474
|
+
}
|
|
475
|
+
function getFieldCacheKey(field) {
|
|
476
|
+
return 'sourceKey' in field && field.sourceKey ? field.sourceKey : field.name;
|
|
477
|
+
}
|
|
478
|
+
function getBelongsToField(context) {
|
|
479
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
480
|
+
const {
|
|
481
|
+
field,
|
|
482
|
+
resourceKey,
|
|
483
|
+
store
|
|
484
|
+
} = context;
|
|
485
|
+
const {
|
|
486
|
+
schema,
|
|
487
|
+
cache
|
|
488
|
+
} = store;
|
|
489
|
+
if (field.options.linksMode) {
|
|
490
|
+
const rawValue = context.editable ? cache.getRelationship(resourceKey, getFieldCacheKeyStrict(field)) : cache.getRemoteRelationship(resourceKey, getFieldCacheKeyStrict(field));
|
|
491
|
+
return rawValue.data ? store.peekRecord(rawValue.data) : null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// FIXME move this to a "LegacyMode" make this part of "PolarisMode"
|
|
495
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
496
|
+
if (!test) {
|
|
497
|
+
throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
|
|
498
|
+
}
|
|
499
|
+
})(context.legacy) : {};
|
|
500
|
+
return schema._kind('@legacy', 'belongsTo').get(store, context.record, resourceKey, field);
|
|
501
|
+
}
|
|
502
|
+
function setBelongsToField(context) {
|
|
503
|
+
const {
|
|
504
|
+
store
|
|
505
|
+
} = context;
|
|
506
|
+
const {
|
|
507
|
+
schema
|
|
508
|
+
} = store;
|
|
509
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
510
|
+
if (!test) {
|
|
511
|
+
throw new Error(`Can only mutate belongsTo fields when the resource is in legacy mode`);
|
|
512
|
+
}
|
|
513
|
+
})(context.legacy) : {};
|
|
514
|
+
schema._kind('@legacy', 'belongsTo').set(store, context.record, context.resourceKey, context.field, context.value);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
function getCollectionField(context) {
|
|
518
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
519
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
520
|
+
{
|
|
521
|
+
throw new Error(`Accessing collection fields is not yet implemented`);
|
|
522
|
+
}
|
|
523
|
+
})() : {};
|
|
524
|
+
}
|
|
525
|
+
function setCollectionField(context) {
|
|
526
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
527
|
+
{
|
|
528
|
+
throw new Error(`Setting collection fields is not yet implemented`);
|
|
529
|
+
}
|
|
530
|
+
})() : {};
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
function getDerivedField(context) {
|
|
534
|
+
const {
|
|
535
|
+
schema
|
|
536
|
+
} = context.store;
|
|
537
|
+
return schema.derivation(context.field)(context.record, context.field.options ?? null, context.field.name);
|
|
538
|
+
}
|
|
539
|
+
function setDerivedField(context) {
|
|
540
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
541
|
+
{
|
|
542
|
+
throw new Error(`ILLEGAL SET: Cannot set '${context.path.join('.')}' on '${context.resourceKey.type}' as ${context.field.kind} fields are not mutable`);
|
|
543
|
+
}
|
|
544
|
+
})() : {};
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
function getGenericField(context) {
|
|
548
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
549
|
+
const {
|
|
550
|
+
cache,
|
|
551
|
+
schema
|
|
552
|
+
} = context.store;
|
|
553
|
+
const rawValue = context.editable ? cache.getAttr(context.resourceKey, context.path) : cache.getRemoteAttr(context.resourceKey, context.path);
|
|
554
|
+
const {
|
|
555
|
+
field
|
|
556
|
+
} = context;
|
|
557
|
+
if (!field.type) {
|
|
558
|
+
return rawValue;
|
|
559
|
+
}
|
|
560
|
+
const transform = schema.transformation(field);
|
|
561
|
+
return transform.hydrate(rawValue, field.options ?? null, context.record);
|
|
562
|
+
}
|
|
563
|
+
function setGenericField(context) {
|
|
564
|
+
const {
|
|
565
|
+
cache,
|
|
566
|
+
schema
|
|
567
|
+
} = context.store;
|
|
568
|
+
const {
|
|
569
|
+
field
|
|
570
|
+
} = context;
|
|
571
|
+
if (!field.type) {
|
|
572
|
+
cache.setAttr(context.resourceKey, context.path, context.value);
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
const transform = schema.transformation(field);
|
|
576
|
+
const rawValue = transform.serialize(context.value, field.options ?? null, context.record);
|
|
577
|
+
cache.setAttr(context.resourceKey, context.path, rawValue);
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
class ManyArrayManager {
|
|
581
|
+
constructor(record, editable) {
|
|
582
|
+
this.record = record;
|
|
583
|
+
this.store = record[RecordStore];
|
|
584
|
+
this.identifier = record[Identifier];
|
|
585
|
+
this.editable = editable;
|
|
586
|
+
}
|
|
587
|
+
_syncArray(array) {
|
|
588
|
+
const method = this.editable ? 'getRelationship' : 'getRemoteRelationship';
|
|
589
|
+
// FIXME field needs to use sourceKey
|
|
590
|
+
const rawValue = this.store.cache[method](this.identifier, array.key);
|
|
591
|
+
if (rawValue.meta) {
|
|
592
|
+
array.meta = rawValue.meta;
|
|
593
|
+
}
|
|
594
|
+
if (rawValue.links) {
|
|
595
|
+
array.links = rawValue.links;
|
|
596
|
+
}
|
|
597
|
+
const currentState = array[SOURCE$1];
|
|
598
|
+
|
|
599
|
+
// unlike in the normal RecordArray case, we don't need to divorce the reference
|
|
600
|
+
// because we don't need to worry about associate/disassociate since the graph
|
|
601
|
+
// takes care of that for us
|
|
602
|
+
if (currentState !== rawValue.data) {
|
|
603
|
+
currentState.length = 0;
|
|
604
|
+
fastPush(currentState, rawValue.data);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
reloadHasMany(key, options) {
|
|
608
|
+
// FIXME field needs to use sourceKey
|
|
609
|
+
const field = this.store.schema.fields(this.identifier).get(key);
|
|
610
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
611
|
+
if (!test) {
|
|
612
|
+
throw new Error(`Expected a hasMany field for ${key}`);
|
|
613
|
+
}
|
|
614
|
+
})(field?.kind === 'hasMany') : {};
|
|
615
|
+
const cacheOptions = options ? extractCacheOptions(options) : {
|
|
616
|
+
reload: true
|
|
617
|
+
};
|
|
618
|
+
cacheOptions.types = [field.type];
|
|
619
|
+
const rawValue = this.store.cache.getRelationship(this.identifier, key);
|
|
620
|
+
const req = {
|
|
621
|
+
url: getRelatedLink(rawValue),
|
|
622
|
+
op: 'findHasMany',
|
|
623
|
+
method: 'GET',
|
|
624
|
+
records: rawValue.data,
|
|
625
|
+
cacheOptions,
|
|
626
|
+
options: {
|
|
627
|
+
field,
|
|
628
|
+
identifier: this.identifier,
|
|
629
|
+
links: rawValue.links,
|
|
630
|
+
meta: rawValue.meta
|
|
631
|
+
},
|
|
632
|
+
[EnableHydration]: false
|
|
633
|
+
};
|
|
634
|
+
return this.store.request(req);
|
|
635
|
+
}
|
|
636
|
+
mutate(mutation) {
|
|
637
|
+
this.store.cache.mutate(mutation);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function getRelatedLink(resource) {
|
|
641
|
+
const related = resource.links?.related;
|
|
642
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
643
|
+
if (!test) {
|
|
644
|
+
throw new Error(`Expected a related link`);
|
|
645
|
+
}
|
|
646
|
+
})(related) : {};
|
|
647
|
+
return typeof related === 'object' ? related.href : related;
|
|
648
|
+
}
|
|
649
|
+
function extractCacheOptions(options) {
|
|
650
|
+
const cacheOptions = {};
|
|
651
|
+
if ('reload' in options) {
|
|
652
|
+
cacheOptions.reload = options.reload;
|
|
653
|
+
}
|
|
654
|
+
if ('backgroundReload' in options) {
|
|
655
|
+
cacheOptions.backgroundReload = options.backgroundReload;
|
|
656
|
+
}
|
|
657
|
+
return cacheOptions;
|
|
658
|
+
}
|
|
659
|
+
function getHasManyField(context) {
|
|
660
|
+
const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
661
|
+
const {
|
|
662
|
+
store,
|
|
663
|
+
field
|
|
664
|
+
} = context;
|
|
665
|
+
if (field.options.linksMode) {
|
|
666
|
+
const {
|
|
667
|
+
record
|
|
668
|
+
} = context;
|
|
669
|
+
// the thing we hand out needs to know its owner and path in a private manner
|
|
670
|
+
// its "address" is the parent identifier (identifier) + field name (field.name)
|
|
671
|
+
// in the nested object case field name here is the full dot path from root resource to this value
|
|
672
|
+
// its "key" is the field on the parent record
|
|
673
|
+
// its "owner" is the parent record
|
|
674
|
+
|
|
675
|
+
const cached = signal.value;
|
|
676
|
+
if (cached) {
|
|
677
|
+
return cached;
|
|
678
|
+
}
|
|
679
|
+
const {
|
|
680
|
+
editable,
|
|
681
|
+
resourceKey
|
|
682
|
+
} = context;
|
|
683
|
+
const {
|
|
684
|
+
cache
|
|
685
|
+
} = store;
|
|
686
|
+
const rawValue = cache.getRelationship(resourceKey, getFieldCacheKeyStrict(field));
|
|
687
|
+
if (!rawValue) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
const managedArray = new RelatedCollection({
|
|
691
|
+
store,
|
|
692
|
+
type: field.type,
|
|
693
|
+
identifier: resourceKey,
|
|
694
|
+
cache,
|
|
695
|
+
field: context.legacy ? field : undefined,
|
|
696
|
+
// we divorce the reference here because ManyArray mutates the target directly
|
|
697
|
+
// before sending the mutation op to the cache. We may be able to avoid this in the future
|
|
698
|
+
identifiers: rawValue.data?.slice(),
|
|
699
|
+
key: field.name,
|
|
700
|
+
meta: rawValue.meta || null,
|
|
701
|
+
links: rawValue.links || null,
|
|
702
|
+
isPolymorphic: field.options.polymorphic ?? false,
|
|
703
|
+
isAsync: field.options.async ?? false,
|
|
704
|
+
// TODO: Grab the proper value
|
|
705
|
+
_inverseIsAsync: false,
|
|
706
|
+
// @ts-expect-error Typescript doesn't have a way for us to thread the generic backwards so it infers unknown instead of T
|
|
707
|
+
manager: new ManyArrayManager(record, editable),
|
|
708
|
+
isLoaded: true,
|
|
709
|
+
allowMutation: editable
|
|
710
|
+
});
|
|
711
|
+
signal.value = managedArray;
|
|
712
|
+
return managedArray;
|
|
713
|
+
}
|
|
714
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
715
|
+
if (!test) {
|
|
716
|
+
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
717
|
+
}
|
|
718
|
+
})(context.legacy) : {};
|
|
719
|
+
return store.schema._kind('@legacy', 'hasMany').get(store, context.record, context.resourceKey, field);
|
|
720
|
+
}
|
|
721
|
+
function setHasManyField(context) {
|
|
722
|
+
const {
|
|
723
|
+
store
|
|
724
|
+
} = context;
|
|
725
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
726
|
+
if (!test) {
|
|
727
|
+
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
728
|
+
}
|
|
729
|
+
})(context.legacy) : {};
|
|
730
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
731
|
+
if (!test) {
|
|
732
|
+
throw new Error(`You must pass an array of records to set a hasMany relationship`);
|
|
733
|
+
}
|
|
734
|
+
})(Array.isArray(context.value)) : {};
|
|
735
|
+
store.schema._kind('@legacy', 'hasMany').set(store, context.record, context.resourceKey, context.field, context.value);
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
function getHashField(context) {
|
|
739
|
+
const {
|
|
740
|
+
field,
|
|
741
|
+
path,
|
|
742
|
+
resourceKey
|
|
743
|
+
} = context;
|
|
744
|
+
const {
|
|
745
|
+
schema,
|
|
746
|
+
cache
|
|
747
|
+
} = context.store;
|
|
748
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
749
|
+
if (!test) {
|
|
750
|
+
throw new Error(`Cannot use a ${field.kind} directly on a resource.`);
|
|
751
|
+
}
|
|
752
|
+
})(Array.isArray(path) && path.length > 1) : {};
|
|
753
|
+
const realPath = path.slice(0, -1);
|
|
754
|
+
const rawData = context.editable ? cache.getAttr(resourceKey, realPath) : cache.getRemoteAttr(resourceKey, realPath);
|
|
755
|
+
return schema.hashFn(field)(rawData, field.options ?? null, field.name ?? null);
|
|
756
|
+
}
|
|
757
|
+
function setHashField(context) {
|
|
758
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
759
|
+
{
|
|
760
|
+
throw new Error(`ILLEGAL SET: Cannot set '${Array.isArray(context.path) ? context.path.join('.') : context.path}' on '${context.resourceKey.type}' as ${context.field.kind} fields are not mutable`);
|
|
761
|
+
}
|
|
762
|
+
})() : {};
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
function getIdentityField(context) {
|
|
766
|
+
entangleSignal(context.signals, context.record, '@identity', null);
|
|
767
|
+
return context.resourceKey.id;
|
|
768
|
+
}
|
|
769
|
+
function setIdentityField(context) {
|
|
770
|
+
const {
|
|
771
|
+
value,
|
|
772
|
+
resourceKey,
|
|
773
|
+
store
|
|
774
|
+
} = context;
|
|
775
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
776
|
+
if (!test) {
|
|
777
|
+
throw new Error(`Expected to receive a string id`);
|
|
778
|
+
}
|
|
779
|
+
})(typeof value === 'string' && value.length) : {};
|
|
780
|
+
const normalizedId = String(value);
|
|
781
|
+
const didChange = normalizedId !== resourceKey.id;
|
|
782
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
783
|
+
if (!test) {
|
|
784
|
+
throw new Error(`Cannot set ${resourceKey.type} record's id to ${normalizedId}, because id is already ${resourceKey.id}`);
|
|
785
|
+
}
|
|
786
|
+
})(!didChange || resourceKey.id === null) : {};
|
|
787
|
+
if (normalizedId !== null && didChange) {
|
|
788
|
+
store._instanceCache.setRecordId(resourceKey, normalizedId);
|
|
789
|
+
store.notifications.notify(resourceKey, 'identity');
|
|
790
|
+
}
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
function getLocalField(context) {
|
|
794
|
+
const {
|
|
795
|
+
field
|
|
796
|
+
} = context;
|
|
797
|
+
const signal = getOrCreateInternalSignal(context.signals, context.record, field.name, field.options?.defaultValue ?? null);
|
|
798
|
+
consumeInternalSignal(signal);
|
|
799
|
+
return signal.value;
|
|
800
|
+
}
|
|
801
|
+
function setLocalField(context) {
|
|
802
|
+
const {
|
|
803
|
+
value
|
|
804
|
+
} = context;
|
|
805
|
+
const signal = getOrCreateInternalSignal(context.signals, context.record, context.field.name, value);
|
|
806
|
+
if (signal.value !== value) {
|
|
807
|
+
signal.value = value;
|
|
808
|
+
notifyInternalSignal(signal);
|
|
809
|
+
}
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
305
812
|
const ObjectSymbols = new Set([OBJECT_SIGNAL, Parent, SOURCE, Editable, EmbeddedPath]);
|
|
306
813
|
|
|
307
814
|
// const ignoredGlobalFields = new Set<string>(['setInterval', 'nodeType', 'nodeName', 'length', 'document', STRUCTURED]);
|
|
308
815
|
|
|
309
816
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
310
817
|
class ManagedObject {
|
|
311
|
-
constructor(
|
|
818
|
+
constructor(context) {
|
|
819
|
+
const {
|
|
820
|
+
field,
|
|
821
|
+
path
|
|
822
|
+
} = context;
|
|
312
823
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
313
824
|
const self = this;
|
|
314
|
-
this[SOURCE] = {
|
|
315
|
-
...data
|
|
316
|
-
};
|
|
825
|
+
this[SOURCE] = Object.assign({}, context.value);
|
|
317
826
|
const signals = withSignalStore(this);
|
|
318
827
|
const _SIGNAL = this[OBJECT_SIGNAL] = entangleSignal(signals, this, OBJECT_SIGNAL, undefined);
|
|
319
|
-
this[Editable] = editable;
|
|
320
|
-
this[Legacy] = legacy;
|
|
321
|
-
this[Parent] =
|
|
828
|
+
this[Editable] = context.editable;
|
|
829
|
+
this[Legacy] = context.legacy;
|
|
830
|
+
this[Parent] = context.resourceKey;
|
|
322
831
|
this[EmbeddedPath] = path;
|
|
832
|
+
const identifier = context.resourceKey;
|
|
833
|
+
const {
|
|
834
|
+
cache,
|
|
835
|
+
schema
|
|
836
|
+
} = context.store;
|
|
323
837
|
|
|
324
838
|
// prettier-ignore
|
|
325
|
-
const extensions = !legacy ? null : schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(field);
|
|
839
|
+
const extensions = !context.legacy ? null : schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, null);
|
|
326
840
|
const proxy = new Proxy(this[SOURCE], {
|
|
327
841
|
ownKeys() {
|
|
328
842
|
return Object.keys(self[SOURCE]);
|
|
@@ -332,7 +846,7 @@ class ManagedObject {
|
|
|
332
846
|
},
|
|
333
847
|
getOwnPropertyDescriptor(target, prop) {
|
|
334
848
|
return {
|
|
335
|
-
writable: editable,
|
|
849
|
+
writable: context.editable,
|
|
336
850
|
enumerable: true,
|
|
337
851
|
configurable: true
|
|
338
852
|
};
|
|
@@ -366,11 +880,9 @@ class ManagedObject {
|
|
|
366
880
|
if (newData && newData !== self[SOURCE]) {
|
|
367
881
|
if (field.type) {
|
|
368
882
|
const transform = schema.transformation(field);
|
|
369
|
-
newData = transform.hydrate(newData, field.options ?? null,
|
|
883
|
+
newData = transform.hydrate(newData, field.options ?? null, context.record);
|
|
370
884
|
}
|
|
371
|
-
self[SOURCE] = {
|
|
372
|
-
...newData
|
|
373
|
-
}; // Add type assertion for newData
|
|
885
|
+
self[SOURCE] = Object.assign({}, newData); // Add type assertion for newData
|
|
374
886
|
}
|
|
375
887
|
}
|
|
376
888
|
|
|
@@ -396,7 +908,7 @@ class ManagedObject {
|
|
|
396
908
|
if (!test) {
|
|
397
909
|
throw new Error(`Cannot set read-only property '${String(prop)}' on ManagedObject`);
|
|
398
910
|
}
|
|
399
|
-
})(editable) : {};
|
|
911
|
+
})(context.editable) : {};
|
|
400
912
|
|
|
401
913
|
// since objects function as dictionaries, we can't defer to schema/data before extensions
|
|
402
914
|
// unless the prop is in the existing data.
|
|
@@ -411,7 +923,7 @@ class ManagedObject {
|
|
|
411
923
|
cache.setAttr(identifier, path, self[SOURCE]);
|
|
412
924
|
} else {
|
|
413
925
|
const transform = schema.transformation(field);
|
|
414
|
-
const val = transform.serialize(self[SOURCE], field.options ?? null,
|
|
926
|
+
const val = transform.serialize(self[SOURCE], field.options ?? null, context.record);
|
|
415
927
|
cache.setAttr(identifier, path, val);
|
|
416
928
|
}
|
|
417
929
|
_SIGNAL.isStale = true;
|
|
@@ -421,141 +933,19 @@ class ManagedObject {
|
|
|
421
933
|
return proxy;
|
|
422
934
|
}
|
|
423
935
|
}
|
|
424
|
-
class ManyArrayManager {
|
|
425
|
-
constructor(record, editable) {
|
|
426
|
-
this.record = record;
|
|
427
|
-
this.store = record[RecordStore];
|
|
428
|
-
this.identifier = record[Identifier];
|
|
429
|
-
this.editable = editable;
|
|
430
|
-
}
|
|
431
|
-
_syncArray(array) {
|
|
432
|
-
const method = this.editable ? 'getRelationship' : 'getRemoteRelationship';
|
|
433
|
-
const rawValue = this.store.cache[method](this.identifier, array.key);
|
|
434
|
-
if (rawValue.meta) {
|
|
435
|
-
array.meta = rawValue.meta;
|
|
436
|
-
}
|
|
437
|
-
if (rawValue.links) {
|
|
438
|
-
array.links = rawValue.links;
|
|
439
|
-
}
|
|
440
|
-
const currentState = array[SOURCE$1];
|
|
441
|
-
|
|
442
|
-
// unlike in the normal RecordArray case, we don't need to divorce the reference
|
|
443
|
-
// because we don't need to worry about associate/disassociate since the graph
|
|
444
|
-
// takes care of that for us
|
|
445
|
-
if (currentState !== rawValue.data) {
|
|
446
|
-
currentState.length = 0;
|
|
447
|
-
fastPush(currentState, rawValue.data);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
reloadHasMany(key, options) {
|
|
451
|
-
const field = this.store.schema.fields(this.identifier).get(key);
|
|
452
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
453
|
-
if (!test) {
|
|
454
|
-
throw new Error(`Expected a hasMany field for ${key}`);
|
|
455
|
-
}
|
|
456
|
-
})(field?.kind === 'hasMany') : {};
|
|
457
|
-
const cacheOptions = options ? extractCacheOptions(options) : {
|
|
458
|
-
reload: true
|
|
459
|
-
};
|
|
460
|
-
cacheOptions.types = [field.type];
|
|
461
|
-
const rawValue = this.store.cache.getRelationship(this.identifier, key);
|
|
462
|
-
const req = {
|
|
463
|
-
url: getRelatedLink(rawValue),
|
|
464
|
-
op: 'findHasMany',
|
|
465
|
-
method: 'GET',
|
|
466
|
-
records: rawValue.data,
|
|
467
|
-
cacheOptions,
|
|
468
|
-
options: {
|
|
469
|
-
field,
|
|
470
|
-
identifier: this.identifier,
|
|
471
|
-
links: rawValue.links,
|
|
472
|
-
meta: rawValue.meta
|
|
473
|
-
},
|
|
474
|
-
[EnableHydration]: false
|
|
475
|
-
};
|
|
476
|
-
return this.store.request(req);
|
|
477
|
-
}
|
|
478
|
-
mutate(mutation) {
|
|
479
|
-
this.store.cache.mutate(mutation);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
function getRelatedLink(resource) {
|
|
483
|
-
const related = resource.links?.related;
|
|
484
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
485
|
-
if (!test) {
|
|
486
|
-
throw new Error(`Expected a related link`);
|
|
487
|
-
}
|
|
488
|
-
})(related) : {};
|
|
489
|
-
return typeof related === 'object' ? related.href : related;
|
|
490
|
-
}
|
|
491
|
-
function extractCacheOptions(options) {
|
|
492
|
-
const cacheOptions = {};
|
|
493
|
-
if ('reload' in options) {
|
|
494
|
-
cacheOptions.reload = options.reload;
|
|
495
|
-
}
|
|
496
|
-
if ('backgroundReload' in options) {
|
|
497
|
-
cacheOptions.backgroundReload = options.backgroundReload;
|
|
498
|
-
}
|
|
499
|
-
return cacheOptions;
|
|
500
|
-
}
|
|
501
|
-
const ManagedArrayMap = getOrSetGlobal('ManagedArrayMap', new Map());
|
|
502
936
|
const ManagedObjectMap = getOrSetGlobal('ManagedObjectMap', new Map());
|
|
503
|
-
function computeLocal(record, field, prop) {
|
|
504
|
-
const signals = withSignalStore(record);
|
|
505
|
-
const signal = getOrCreateInternalSignal(signals, record, prop, field.options?.defaultValue ?? null);
|
|
506
|
-
consumeInternalSignal(signal);
|
|
507
|
-
return signal.value;
|
|
508
|
-
}
|
|
509
|
-
function peekManagedArray(record, field) {
|
|
510
|
-
const managedArrayMapForRecord = ManagedArrayMap.get(record);
|
|
511
|
-
if (managedArrayMapForRecord) {
|
|
512
|
-
return managedArrayMapForRecord.get(field.name);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
937
|
function peekManagedObject(record, field) {
|
|
516
938
|
const managedObjectMapForRecord = ManagedObjectMap.get(record);
|
|
517
939
|
if (managedObjectMapForRecord) {
|
|
518
940
|
return managedObjectMapForRecord.get(field.name);
|
|
519
941
|
}
|
|
520
942
|
}
|
|
521
|
-
function
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return transform.hydrate(rawValue, field.options ?? null, record);
|
|
528
|
-
}
|
|
529
|
-
function computeArray(store, schema, cache, record, identifier, field, path, editable, legacy) {
|
|
530
|
-
const isSchemaArray = field.kind === 'schema-array';
|
|
531
|
-
// the thing we hand out needs to know its owner and path in a private manner
|
|
532
|
-
// its "address" is the parent identifier (identifier) + field name (field.name)
|
|
533
|
-
// in the nested object case field name here is the full dot path from root resource to this value
|
|
534
|
-
// its "key" is the field on the parent record
|
|
535
|
-
// its "owner" is the parent record
|
|
536
|
-
|
|
537
|
-
const managedArrayMapForRecord = ManagedArrayMap.get(record);
|
|
538
|
-
let managedArray;
|
|
539
|
-
if (managedArrayMapForRecord) {
|
|
540
|
-
managedArray = managedArrayMapForRecord.get(field.name);
|
|
541
|
-
}
|
|
542
|
-
if (managedArray) {
|
|
543
|
-
return managedArray;
|
|
544
|
-
} else {
|
|
545
|
-
const rawValue = editable ? cache.getAttr(identifier, path) : cache.getRemoteAttr(identifier, path);
|
|
546
|
-
if (!rawValue) {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
managedArray = new ManagedArray(store, schema, cache, field, rawValue, identifier, path, record, isSchemaArray, editable, legacy);
|
|
550
|
-
if (!managedArrayMapForRecord) {
|
|
551
|
-
ManagedArrayMap.set(record, new Map([[field.name, managedArray]]));
|
|
552
|
-
} else {
|
|
553
|
-
managedArrayMapForRecord.set(field.name, managedArray);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return managedArray;
|
|
557
|
-
}
|
|
558
|
-
function computeObject(schema, cache, record, identifier, field, path, editable, legacy) {
|
|
943
|
+
function getObjectField(context) {
|
|
944
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
945
|
+
const {
|
|
946
|
+
record,
|
|
947
|
+
field
|
|
948
|
+
} = context;
|
|
559
949
|
const managedObjectMapForRecord = ManagedObjectMap.get(record);
|
|
560
950
|
let managedObject;
|
|
561
951
|
if (managedObjectMapForRecord) {
|
|
@@ -564,7 +954,16 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
|
|
|
564
954
|
if (managedObject) {
|
|
565
955
|
return managedObject;
|
|
566
956
|
} else {
|
|
567
|
-
|
|
957
|
+
const {
|
|
958
|
+
store,
|
|
959
|
+
resourceKey,
|
|
960
|
+
path
|
|
961
|
+
} = context;
|
|
962
|
+
const {
|
|
963
|
+
cache,
|
|
964
|
+
schema
|
|
965
|
+
} = store;
|
|
966
|
+
let rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
|
|
568
967
|
if (!rawValue) {
|
|
569
968
|
return null;
|
|
570
969
|
}
|
|
@@ -572,7 +971,18 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
|
|
|
572
971
|
const transform = schema.transformation(field);
|
|
573
972
|
rawValue = transform.hydrate(rawValue, field.options ?? null, record);
|
|
574
973
|
}
|
|
575
|
-
managedObject = new ManagedObject(
|
|
974
|
+
managedObject = new ManagedObject({
|
|
975
|
+
store,
|
|
976
|
+
resourceKey,
|
|
977
|
+
modeName: context.modeName,
|
|
978
|
+
legacy: context.legacy,
|
|
979
|
+
editable: context.editable,
|
|
980
|
+
path,
|
|
981
|
+
field,
|
|
982
|
+
record,
|
|
983
|
+
signals: context.signals,
|
|
984
|
+
value: rawValue
|
|
985
|
+
});
|
|
576
986
|
if (!managedObjectMapForRecord) {
|
|
577
987
|
ManagedObjectMap.set(record, new Map([[field.name, managedObject]]));
|
|
578
988
|
} else {
|
|
@@ -581,48 +991,64 @@ function computeObject(schema, cache, record, identifier, field, path, editable,
|
|
|
581
991
|
}
|
|
582
992
|
return managedObject;
|
|
583
993
|
}
|
|
584
|
-
function
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
994
|
+
function setObjectField(context) {
|
|
995
|
+
const {
|
|
996
|
+
field,
|
|
997
|
+
value,
|
|
998
|
+
record
|
|
999
|
+
} = context;
|
|
1000
|
+
const {
|
|
1001
|
+
cache,
|
|
1002
|
+
schema
|
|
1003
|
+
} = context.store;
|
|
1004
|
+
if (!field.type) {
|
|
1005
|
+
let newValue = value;
|
|
1006
|
+
if (value !== null) {
|
|
1007
|
+
newValue = {
|
|
1008
|
+
...value
|
|
1009
|
+
};
|
|
1010
|
+
} else {
|
|
1011
|
+
ManagedObjectMap.delete(record);
|
|
596
1012
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
[
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
ManagedObjectMap.set(record, new Map([[field.name, schemaObject]]));
|
|
605
|
-
} else {
|
|
606
|
-
schemaObjectMapForRecord.set(field.name, schemaObject);
|
|
1013
|
+
cache.setAttr(context.resourceKey, context.path, newValue);
|
|
1014
|
+
const peeked = peekManagedObject(record, field);
|
|
1015
|
+
if (peeked) {
|
|
1016
|
+
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1017
|
+
objSignal.isStale = true;
|
|
1018
|
+
}
|
|
1019
|
+
return true;
|
|
607
1020
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1021
|
+
const transform = schema.transformation(field);
|
|
1022
|
+
const rawValue = transform.serialize({
|
|
1023
|
+
...value
|
|
1024
|
+
}, field.options ?? null, record);
|
|
1025
|
+
cache.setAttr(context.resourceKey, context.path, rawValue);
|
|
1026
|
+
const peeked = peekManagedObject(record, field);
|
|
1027
|
+
if (peeked) {
|
|
1028
|
+
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1029
|
+
objSignal.isStale = true;
|
|
1030
|
+
}
|
|
1031
|
+
return true;
|
|
615
1032
|
}
|
|
1033
|
+
|
|
616
1034
|
// TODO probably this should just be a Document
|
|
617
1035
|
// but its separate until we work out the lid situation
|
|
618
1036
|
class ResourceRelationship {
|
|
619
|
-
constructor(
|
|
620
|
-
const
|
|
1037
|
+
constructor(context) {
|
|
1038
|
+
const {
|
|
1039
|
+
store,
|
|
1040
|
+
resourceKey
|
|
1041
|
+
} = context;
|
|
1042
|
+
const {
|
|
1043
|
+
cache
|
|
1044
|
+
} = store;
|
|
1045
|
+
const name = getFieldCacheKeyStrict(context.field);
|
|
1046
|
+
const rawValue = context.editable ? cache.getRelationship(resourceKey, name) : cache.getRemoteRelationship(resourceKey, name);
|
|
621
1047
|
|
|
622
1048
|
// TODO setup true lids for relationship documents
|
|
623
1049
|
// @ts-expect-error we need to give relationship documents a lid
|
|
624
1050
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
625
|
-
this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${
|
|
1051
|
+
this.lid = rawValue.lid ?? rawValue.links?.self ?? `relationship:${resourceKey.lid}.${name}`;
|
|
626
1052
|
this.data = rawValue.data ? store.peekRecord(rawValue.data) : null;
|
|
627
1053
|
this.name = name;
|
|
628
1054
|
if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
|
|
@@ -633,7 +1059,7 @@ class ResourceRelationship {
|
|
|
633
1059
|
this.meta = rawValue.meta ?? {};
|
|
634
1060
|
}
|
|
635
1061
|
this[RecordStore] = store;
|
|
636
|
-
this[Parent] =
|
|
1062
|
+
this[Parent] = context.record;
|
|
637
1063
|
}
|
|
638
1064
|
fetch(options) {
|
|
639
1065
|
const url = options?.url ?? getHref(this.links.related) ?? getHref(this.links.self) ?? null;
|
|
@@ -659,60 +1085,273 @@ function getHref(link) {
|
|
|
659
1085
|
}
|
|
660
1086
|
return link.href;
|
|
661
1087
|
}
|
|
662
|
-
function
|
|
663
|
-
|
|
664
|
-
|
|
1088
|
+
function getResourceField(context) {
|
|
1089
|
+
entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
1090
|
+
return new ResourceRelationship(context);
|
|
1091
|
+
}
|
|
1092
|
+
function setResourceField(context) {
|
|
1093
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1094
|
+
{
|
|
1095
|
+
throw new Error(`setting resource relationships is not yet supported`);
|
|
1096
|
+
}
|
|
1097
|
+
})() : {};
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
function setSchemaArrayField(context) {
|
|
1101
|
+
const arrayValue = context.value?.slice();
|
|
1102
|
+
const fieldSignal = peekInternalSignal(context.signals, context.path.at(-1));
|
|
1103
|
+
const peeked = fieldSignal?.value;
|
|
1104
|
+
context.store.cache.setAttr(context.resourceKey, context.path, arrayValue);
|
|
1105
|
+
if (peeked) {
|
|
1106
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1107
|
+
if (!test) {
|
|
1108
|
+
throw new Error(`Expected the peekManagedArray for ${context.field.kind} to return a ManagedArray`);
|
|
1109
|
+
}
|
|
1110
|
+
})(ARRAY_SIGNAL in peeked) : {};
|
|
1111
|
+
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1112
|
+
arrSignal.isStale = true;
|
|
1113
|
+
if (!Array.isArray(arrayValue)) {
|
|
1114
|
+
fieldSignal.value = null;
|
|
1115
|
+
}
|
|
665
1116
|
}
|
|
666
|
-
return
|
|
1117
|
+
return true;
|
|
667
1118
|
}
|
|
668
|
-
function
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1119
|
+
function getSchemaObjectField(context) {
|
|
1120
|
+
const signal = entangleSignal(context.signals, context.record, context.path.at(-1), null);
|
|
1121
|
+
const {
|
|
1122
|
+
store,
|
|
1123
|
+
resourceKey,
|
|
1124
|
+
path
|
|
1125
|
+
} = context;
|
|
1126
|
+
const {
|
|
1127
|
+
cache
|
|
1128
|
+
} = store;
|
|
1129
|
+
const rawValue = context.editable ? cache.getAttr(resourceKey, path) : cache.getRemoteAttr(resourceKey, path);
|
|
1130
|
+
if (!rawValue) {
|
|
1131
|
+
if (signal.value) {
|
|
1132
|
+
const value = signal.value;
|
|
1133
|
+
// TODO if we had idle scheduling this should be done there.
|
|
1134
|
+
void Promise.resolve().then(() => {
|
|
1135
|
+
value.value[Destroy]();
|
|
1136
|
+
});
|
|
1137
|
+
signal.value = null;
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
679
1140
|
}
|
|
680
|
-
|
|
681
|
-
|
|
1141
|
+
const {
|
|
1142
|
+
field
|
|
1143
|
+
} = context;
|
|
1144
|
+
const {
|
|
1145
|
+
schema
|
|
1146
|
+
} = store;
|
|
1147
|
+
let objectType;
|
|
1148
|
+
if (field.options?.polymorphic) {
|
|
1149
|
+
const typePath = field.options.type ?? 'type';
|
|
1150
|
+
// if we are polymorphic, then context.field.options.type will
|
|
1151
|
+
// either specify a path on the rawValue to use as the type, defaulting to "type" or
|
|
1152
|
+
// the special string "@hash" which tells us to treat field.type as a hashFn name with which
|
|
1153
|
+
// to calc the type.
|
|
1154
|
+
if (typePath === '@hash') {
|
|
1155
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1156
|
+
if (!test) {
|
|
1157
|
+
throw new Error(`Expected the field to define a hashFn as its type`);
|
|
1158
|
+
}
|
|
1159
|
+
})(field.type) : {};
|
|
1160
|
+
const hashFn = schema.hashFn({
|
|
1161
|
+
type: field.type
|
|
1162
|
+
});
|
|
1163
|
+
// TODO consider if there are better options and name args we could provide.
|
|
1164
|
+
objectType = hashFn(rawValue, null, null);
|
|
1165
|
+
} else {
|
|
1166
|
+
objectType = rawValue[typePath];
|
|
1167
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1168
|
+
if (!test) {
|
|
1169
|
+
throw new Error(`Expected the type path for the field to be a value on the raw object`);
|
|
1170
|
+
}
|
|
1171
|
+
})(typePath && objectType && typeof objectType === 'string') : {};
|
|
1172
|
+
}
|
|
682
1173
|
} else {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1174
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1175
|
+
if (!test) {
|
|
1176
|
+
throw new Error(`A non-polymorphic SchemaObjectField must provide a SchemaObject type in its definition`);
|
|
1177
|
+
}
|
|
1178
|
+
})(field.type) : {};
|
|
1179
|
+
objectType = field.type;
|
|
1180
|
+
}
|
|
1181
|
+
const hashField = schema.resource({
|
|
1182
|
+
type: objectType
|
|
1183
|
+
}).identity;
|
|
1184
|
+
const identity = hashField ? schema.hashFn(hashField)(rawValue, hashField.options ?? null, hashField.name) : field.name;
|
|
1185
|
+
const cachedSchemaObject = signal.value;
|
|
1186
|
+
if (cachedSchemaObject) {
|
|
1187
|
+
if (cachedSchemaObject.type === objectType && cachedSchemaObject.identity === identity) {
|
|
1188
|
+
return cachedSchemaObject.value;
|
|
1189
|
+
} else {
|
|
1190
|
+
// TODO if we had idle scheduling this should be done there.
|
|
1191
|
+
void Promise.resolve().then(() => {
|
|
1192
|
+
cachedSchemaObject.value[Destroy]();
|
|
1193
|
+
});
|
|
686
1194
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1195
|
+
}
|
|
1196
|
+
const schemaObject = new ReactiveResource({
|
|
1197
|
+
store: context.store,
|
|
1198
|
+
resourceKey: context.resourceKey,
|
|
1199
|
+
modeName: context.modeName,
|
|
1200
|
+
legacy: context.legacy,
|
|
1201
|
+
editable: context.editable,
|
|
1202
|
+
path: context.path,
|
|
1203
|
+
field: context.field,
|
|
1204
|
+
value: objectType
|
|
1205
|
+
});
|
|
1206
|
+
signal.value = {
|
|
1207
|
+
type: objectType,
|
|
1208
|
+
identity: identity,
|
|
1209
|
+
value: schemaObject
|
|
1210
|
+
};
|
|
1211
|
+
return schemaObject;
|
|
1212
|
+
}
|
|
1213
|
+
function setSchemaObjectField(context) {
|
|
1214
|
+
const {
|
|
1215
|
+
store,
|
|
1216
|
+
value
|
|
1217
|
+
} = context;
|
|
1218
|
+
let newValue = value;
|
|
1219
|
+
if (value !== null) {
|
|
1220
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1221
|
+
if (!test) {
|
|
1222
|
+
throw new Error(`Expected value to be an object`);
|
|
1223
|
+
}
|
|
1224
|
+
})(typeof value === 'object') : {};
|
|
1225
|
+
newValue = {
|
|
1226
|
+
...value
|
|
1227
|
+
};
|
|
1228
|
+
// FIXME the case of field.type to string here is likely incorrect
|
|
1229
|
+
const schemaFields = store.schema.fields({
|
|
1230
|
+
type: context.field.type
|
|
707
1231
|
});
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1232
|
+
for (const key of Object.keys(newValue)) {
|
|
1233
|
+
if (!schemaFields.has(key)) {
|
|
1234
|
+
throw new Error(`Field ${key} does not exist on schema object ${context.field.type}`);
|
|
1235
|
+
}
|
|
712
1236
|
}
|
|
1237
|
+
} else {
|
|
1238
|
+
ManagedObjectMap.delete(context.record);
|
|
713
1239
|
}
|
|
714
|
-
|
|
1240
|
+
store.cache.setAttr(context.resourceKey, context.path, newValue);
|
|
1241
|
+
// const peeked = peekManagedObject(self, field);
|
|
1242
|
+
// if (peeked) {
|
|
1243
|
+
// const objSignal = peeked[OBJECT_SIGNAL];
|
|
1244
|
+
// objSignal.isStale = true;
|
|
1245
|
+
// }
|
|
1246
|
+
return true;
|
|
715
1247
|
}
|
|
1248
|
+
const DefaultMode = {
|
|
1249
|
+
'@hash': {
|
|
1250
|
+
get: getHashField,
|
|
1251
|
+
set: setHashField,
|
|
1252
|
+
mutable: false,
|
|
1253
|
+
enumerable: false,
|
|
1254
|
+
serializable: false
|
|
1255
|
+
},
|
|
1256
|
+
'@id': {
|
|
1257
|
+
get: getIdentityField,
|
|
1258
|
+
set: setIdentityField,
|
|
1259
|
+
mutable: true,
|
|
1260
|
+
enumerable: true,
|
|
1261
|
+
serializable: true
|
|
1262
|
+
},
|
|
1263
|
+
'@local': {
|
|
1264
|
+
get: getLocalField,
|
|
1265
|
+
set: setLocalField,
|
|
1266
|
+
mutable: true,
|
|
1267
|
+
enumerable: false,
|
|
1268
|
+
serializable: false
|
|
1269
|
+
},
|
|
1270
|
+
alias: {
|
|
1271
|
+
get: getAliasField,
|
|
1272
|
+
set: setAliasField,
|
|
1273
|
+
mutable: true,
|
|
1274
|
+
enumerable: true,
|
|
1275
|
+
serializable: false
|
|
1276
|
+
},
|
|
1277
|
+
array: {
|
|
1278
|
+
get: getArrayField,
|
|
1279
|
+
set: setArrayField,
|
|
1280
|
+
mutable: true,
|
|
1281
|
+
enumerable: true,
|
|
1282
|
+
serializable: true
|
|
1283
|
+
},
|
|
1284
|
+
attribute: {
|
|
1285
|
+
get: getAttributeField,
|
|
1286
|
+
set: setAttributeField,
|
|
1287
|
+
mutable: true,
|
|
1288
|
+
enumerable: true,
|
|
1289
|
+
serializable: true
|
|
1290
|
+
},
|
|
1291
|
+
belongsTo: {
|
|
1292
|
+
get: getBelongsToField,
|
|
1293
|
+
set: setBelongsToField,
|
|
1294
|
+
mutable: true,
|
|
1295
|
+
enumerable: true,
|
|
1296
|
+
serializable: true
|
|
1297
|
+
},
|
|
1298
|
+
collection: {
|
|
1299
|
+
get: getCollectionField,
|
|
1300
|
+
set: setCollectionField,
|
|
1301
|
+
mutable: true,
|
|
1302
|
+
enumerable: true,
|
|
1303
|
+
serializable: true
|
|
1304
|
+
},
|
|
1305
|
+
derived: {
|
|
1306
|
+
get: getDerivedField,
|
|
1307
|
+
set: setDerivedField,
|
|
1308
|
+
mutable: true,
|
|
1309
|
+
enumerable: true,
|
|
1310
|
+
serializable: false
|
|
1311
|
+
},
|
|
1312
|
+
field: {
|
|
1313
|
+
get: getGenericField,
|
|
1314
|
+
set: setGenericField,
|
|
1315
|
+
mutable: true,
|
|
1316
|
+
enumerable: true,
|
|
1317
|
+
serializable: true
|
|
1318
|
+
},
|
|
1319
|
+
hasMany: {
|
|
1320
|
+
get: getHasManyField,
|
|
1321
|
+
set: setHasManyField,
|
|
1322
|
+
mutable: true,
|
|
1323
|
+
enumerable: true,
|
|
1324
|
+
serializable: true
|
|
1325
|
+
},
|
|
1326
|
+
object: {
|
|
1327
|
+
get: getObjectField,
|
|
1328
|
+
set: setObjectField,
|
|
1329
|
+
mutable: true,
|
|
1330
|
+
enumerable: true,
|
|
1331
|
+
serializable: true
|
|
1332
|
+
},
|
|
1333
|
+
resource: {
|
|
1334
|
+
get: getResourceField,
|
|
1335
|
+
set: setResourceField,
|
|
1336
|
+
mutable: true,
|
|
1337
|
+
enumerable: true,
|
|
1338
|
+
serializable: true
|
|
1339
|
+
},
|
|
1340
|
+
'schema-array': {
|
|
1341
|
+
get: getArrayField,
|
|
1342
|
+
set: setSchemaArrayField,
|
|
1343
|
+
mutable: true,
|
|
1344
|
+
enumerable: true,
|
|
1345
|
+
serializable: true
|
|
1346
|
+
},
|
|
1347
|
+
'schema-object': {
|
|
1348
|
+
get: getSchemaObjectField,
|
|
1349
|
+
set: setSchemaObjectField,
|
|
1350
|
+
mutable: true,
|
|
1351
|
+
enumerable: true,
|
|
1352
|
+
serializable: true
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
716
1355
|
const IgnoredGlobalFields = new Set(['length', 'nodeType', 'then', 'setInterval', 'document', STRUCTURED]);
|
|
717
1356
|
const symbolList = [Destroy, RecordStore, Identifier, Editable, Parent, Checkout, Legacy, EmbeddedPath, EmbeddedField];
|
|
718
1357
|
const RecordSymbols = new Set(symbolList);
|
|
@@ -737,27 +1376,41 @@ const Editables = new WeakMap();
|
|
|
737
1376
|
*/
|
|
738
1377
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
739
1378
|
class ReactiveResource {
|
|
740
|
-
constructor(
|
|
741
|
-
|
|
742
|
-
|
|
1379
|
+
constructor(context) {
|
|
1380
|
+
const {
|
|
1381
|
+
store
|
|
1382
|
+
} = context;
|
|
1383
|
+
const identifier = context.resourceKey;
|
|
1384
|
+
const embeddedField = context.field;
|
|
1385
|
+
const embeddedPath = context.path;
|
|
1386
|
+
const isEmbedded = context.field !== null;
|
|
743
1387
|
this[RecordStore] = store;
|
|
744
1388
|
if (isEmbedded) {
|
|
745
1389
|
this[Parent] = identifier;
|
|
746
1390
|
} else {
|
|
747
1391
|
this[Identifier] = identifier;
|
|
748
1392
|
}
|
|
749
|
-
const IS_EDITABLE = this[Editable] =
|
|
750
|
-
this[Legacy] = Mode[Legacy] ?? false;
|
|
1393
|
+
const IS_EDITABLE = this[Editable] = context.editable ?? false;
|
|
751
1394
|
const schema = store.schema;
|
|
752
|
-
|
|
753
|
-
const
|
|
1395
|
+
this[Legacy] = context.legacy ?? false;
|
|
1396
|
+
const objectType = isEmbedded ? context.value : identifier.type;
|
|
1397
|
+
const ResourceSchema = schema.resource(isEmbedded ? {
|
|
1398
|
+
type: objectType
|
|
1399
|
+
} : identifier);
|
|
1400
|
+
const identityField = ResourceSchema.identity;
|
|
754
1401
|
const BoundFns = new Map();
|
|
755
1402
|
|
|
756
1403
|
// prettier-ignore
|
|
757
|
-
const extensions = !
|
|
1404
|
+
const extensions = !context.legacy ? null : isEmbedded ? schema.CAUTION_MEGA_DANGER_ZONE_objectExtensions(embeddedField, objectType) : schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(identifier);
|
|
758
1405
|
this[EmbeddedField] = embeddedField;
|
|
759
1406
|
this[EmbeddedPath] = embeddedPath;
|
|
760
|
-
const fields = isEmbedded ? schema.fields(
|
|
1407
|
+
const fields = isEmbedded ? schema.fields({
|
|
1408
|
+
type: objectType
|
|
1409
|
+
}) : schema.fields(identifier);
|
|
1410
|
+
const method = typeof schema.cacheFields === 'function' ? 'cacheFields' : 'fields';
|
|
1411
|
+
const cacheFields = isEmbedded ? schema[method]({
|
|
1412
|
+
type: objectType
|
|
1413
|
+
}) : schema[method](identifier);
|
|
761
1414
|
const signals = withSignalStore(this);
|
|
762
1415
|
const proxy = new Proxy(this, {
|
|
763
1416
|
ownKeys() {
|
|
@@ -934,74 +1587,64 @@ class ReactiveResource {
|
|
|
934
1587
|
throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
|
|
935
1588
|
}
|
|
936
1589
|
})(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
|
|
1590
|
+
/**
|
|
1591
|
+
* Prop Array is the path from a resource to the field including
|
|
1592
|
+
* intermediate "links" on arrays,objects,schema-arrays and schema-objects.
|
|
1593
|
+
*
|
|
1594
|
+
* E.g. in the following
|
|
1595
|
+
*
|
|
1596
|
+
* ```
|
|
1597
|
+
* const user = {
|
|
1598
|
+
* addresses: [{
|
|
1599
|
+
* street: 'Sunset Blvd',
|
|
1600
|
+
* zip: 90210
|
|
1601
|
+
* }]
|
|
1602
|
+
* }
|
|
1603
|
+
* ```
|
|
1604
|
+
*
|
|
1605
|
+
* The propArray for "street" is ['addresses', 0, 'street']
|
|
1606
|
+
*
|
|
1607
|
+
* Prop Array follows the `cache` path to the value, not the ui path.
|
|
1608
|
+
* Thus, if `addresses` has a sourceKey of `user_addresses` and
|
|
1609
|
+
* `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
|
|
1610
|
+
* ['user_addresses', 0, 'zip_code']
|
|
1611
|
+
*/
|
|
937
1612
|
const propArray = isEmbedded ? embeddedPath.slice() : [];
|
|
938
1613
|
// we use the field.name instead of prop here because we want to use the cache-path not
|
|
939
1614
|
// the record path.
|
|
940
|
-
|
|
941
|
-
// propArray.
|
|
942
|
-
|
|
1615
|
+
// SAFETY: we lie as string here because if we were to get null
|
|
1616
|
+
// we would be in a field kind that won't use the propArray below.
|
|
1617
|
+
const fieldCacheKey = getFieldCacheKey(field);
|
|
1618
|
+
propArray.push(fieldCacheKey);
|
|
943
1619
|
switch (field.kind) {
|
|
944
1620
|
case '@id':
|
|
945
|
-
entangleSignal(signals, receiver, '@identity', null);
|
|
946
|
-
return identifier.id;
|
|
947
1621
|
case '@hash':
|
|
948
|
-
// TODO pass actual cache value not {}
|
|
949
|
-
return schema.hashFn(field)({}, field.options ?? null, field.name ?? null);
|
|
950
1622
|
case '@local':
|
|
951
|
-
|
|
952
|
-
return computeLocal(receiver, field, prop);
|
|
953
|
-
}
|
|
1623
|
+
case 'derived':
|
|
954
1624
|
case 'field':
|
|
955
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
956
|
-
return computeField(schema, cache, target, identifier, field, propArray, IS_EDITABLE);
|
|
957
1625
|
case 'attribute':
|
|
958
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
959
|
-
return computeAttribute(cache, identifier, prop, IS_EDITABLE);
|
|
960
|
-
case 'resource':
|
|
961
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
962
|
-
return computeResource(store, cache, target, identifier, field, prop, IS_EDITABLE);
|
|
963
|
-
case 'derived':
|
|
964
|
-
return computeDerivation(schema, receiver, identifier, field, prop);
|
|
965
1626
|
case 'schema-array':
|
|
966
1627
|
case 'array':
|
|
967
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
968
|
-
return computeArray(store, schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
|
|
969
|
-
case 'object':
|
|
970
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
971
|
-
return computeObject(schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
|
|
972
1628
|
case 'schema-object':
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
return computeSchemaObject(store, cache, target, identifier, field, propArray, Mode[Legacy], Mode[Editable]);
|
|
1629
|
+
case 'object':
|
|
1630
|
+
case 'resource':
|
|
976
1631
|
case 'belongsTo':
|
|
977
|
-
if (field.options.linksMode) {
|
|
978
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
979
|
-
const rawValue = IS_EDITABLE ? cache.getRelationship(identifier, field.name) : cache.getRemoteRelationship(identifier, field.name);
|
|
980
|
-
|
|
981
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
982
|
-
return rawValue.data ? store.peekRecord(rawValue.data) : null;
|
|
983
|
-
}
|
|
984
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
985
|
-
if (!test) {
|
|
986
|
-
throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
|
|
987
|
-
}
|
|
988
|
-
})(Mode[Legacy]) : {};
|
|
989
|
-
entangleSignal(signals, receiver, field.name, null);
|
|
990
|
-
return schema._kind('@legacy', 'belongsTo').get(store, receiver, identifier, field);
|
|
991
1632
|
case 'hasMany':
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1633
|
+
case 'collection':
|
|
1634
|
+
return DefaultMode[field.kind].get({
|
|
1635
|
+
store,
|
|
1636
|
+
resourceKey: identifier,
|
|
1637
|
+
modeName: context.modeName,
|
|
1638
|
+
legacy: context.legacy,
|
|
1639
|
+
editable: context.editable,
|
|
1640
|
+
path: propArray,
|
|
1641
|
+
field: field,
|
|
1642
|
+
record: receiver,
|
|
1643
|
+
signals,
|
|
1644
|
+
value: null
|
|
1645
|
+
});
|
|
1003
1646
|
default:
|
|
1004
|
-
|
|
1647
|
+
assertNeverField(identifier, field, propArray);
|
|
1005
1648
|
}
|
|
1006
1649
|
},
|
|
1007
1650
|
set(target, prop, value, receiver) {
|
|
@@ -1028,203 +1671,64 @@ class ReactiveResource {
|
|
|
1028
1671
|
throw new Error(`Alias fields cannot alias '@id' '@local' '@hash' or 'derived' fields`);
|
|
1029
1672
|
}
|
|
1030
1673
|
})(maybeField.kind !== 'alias' || !['@id', '@local', '@hash', 'derived'].includes(maybeField.options.kind)) : {};
|
|
1674
|
+
/**
|
|
1675
|
+
* Prop Array is the path from a resource to the field including
|
|
1676
|
+
* intermediate "links" on arrays,objects,schema-arrays and schema-objects.
|
|
1677
|
+
*
|
|
1678
|
+
* E.g. in the following
|
|
1679
|
+
*
|
|
1680
|
+
* ```
|
|
1681
|
+
* const user = {
|
|
1682
|
+
* addresses: [{
|
|
1683
|
+
* street: 'Sunset Blvd',
|
|
1684
|
+
* zip: 90210
|
|
1685
|
+
* }]
|
|
1686
|
+
* }
|
|
1687
|
+
* ```
|
|
1688
|
+
*
|
|
1689
|
+
* The propArray for "street" is ['addresses', 0, 'street']
|
|
1690
|
+
*
|
|
1691
|
+
* Prop Array follows the `cache` path to the value, not the ui path.
|
|
1692
|
+
* Thus, if `addresses` has a sourceKey of `user_addresses` and
|
|
1693
|
+
* `zip` has a sourceKey of `zip_code` then the propArray for "zip" is
|
|
1694
|
+
* ['user_addresses', 0, 'zip_code']
|
|
1695
|
+
*/
|
|
1031
1696
|
const propArray = isEmbedded ? embeddedPath.slice() : [];
|
|
1032
1697
|
// we use the field.name instead of prop here because we want to use the cache-path not
|
|
1033
1698
|
// the record path.
|
|
1034
|
-
|
|
1035
|
-
// propArray.
|
|
1036
|
-
|
|
1699
|
+
// SAFETY: we lie as string here because if we were to get null
|
|
1700
|
+
// we would be in a field kind that won't use the propArray below.
|
|
1701
|
+
const fieldCacheKey = getFieldCacheKey(field);
|
|
1702
|
+
propArray.push(fieldCacheKey);
|
|
1037
1703
|
switch (field.kind) {
|
|
1038
1704
|
case '@id':
|
|
1039
|
-
|
|
1040
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1041
|
-
if (!test) {
|
|
1042
|
-
throw new Error(`Expected to receive a string id`);
|
|
1043
|
-
}
|
|
1044
|
-
})(typeof value === 'string' && value.length) : {};
|
|
1045
|
-
const normalizedId = String(value);
|
|
1046
|
-
const didChange = normalizedId !== identifier.id;
|
|
1047
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1048
|
-
if (!test) {
|
|
1049
|
-
throw new Error(`Cannot set ${identifier.type} record's id to ${normalizedId}, because id is already ${identifier.id}`);
|
|
1050
|
-
}
|
|
1051
|
-
})(!didChange || identifier.id === null) : {};
|
|
1052
|
-
if (normalizedId !== null && didChange) {
|
|
1053
|
-
store._instanceCache.setRecordId(identifier, normalizedId);
|
|
1054
|
-
store.notifications.notify(identifier, 'identity');
|
|
1055
|
-
}
|
|
1056
|
-
return true;
|
|
1057
|
-
}
|
|
1705
|
+
case '@hash':
|
|
1058
1706
|
case '@local':
|
|
1059
|
-
{
|
|
1060
|
-
const signal = getOrCreateInternalSignal(signals, receiver, prop, field.options?.defaultValue ?? null);
|
|
1061
|
-
if (signal.value !== value) {
|
|
1062
|
-
signal.value = value;
|
|
1063
|
-
notifyInternalSignal(signal);
|
|
1064
|
-
}
|
|
1065
|
-
return true;
|
|
1066
|
-
}
|
|
1067
1707
|
case 'field':
|
|
1068
|
-
{
|
|
1069
|
-
if (!field.type) {
|
|
1070
|
-
cache.setAttr(identifier, propArray, value);
|
|
1071
|
-
return true;
|
|
1072
|
-
}
|
|
1073
|
-
const transform = schema.transformation(field);
|
|
1074
|
-
const rawValue = transform.serialize(value, field.options ?? null, target);
|
|
1075
|
-
cache.setAttr(identifier, propArray, rawValue);
|
|
1076
|
-
return true;
|
|
1077
|
-
}
|
|
1078
1708
|
case 'attribute':
|
|
1079
|
-
|
|
1080
|
-
cache.setAttr(identifier, propArray, value);
|
|
1081
|
-
return true;
|
|
1082
|
-
}
|
|
1709
|
+
case 'derived':
|
|
1083
1710
|
case 'array':
|
|
1084
|
-
{
|
|
1085
|
-
if (!field.type) {
|
|
1086
|
-
cache.setAttr(identifier, propArray, value?.slice());
|
|
1087
|
-
const peeked = peekManagedArray(self, field);
|
|
1088
|
-
if (peeked) {
|
|
1089
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1090
|
-
if (!test) {
|
|
1091
|
-
throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
|
|
1092
|
-
}
|
|
1093
|
-
})(ARRAY_SIGNAL in peeked) : {};
|
|
1094
|
-
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1095
|
-
arrSignal.isStale = true;
|
|
1096
|
-
}
|
|
1097
|
-
if (!Array.isArray(value)) {
|
|
1098
|
-
ManagedArrayMap.delete(target);
|
|
1099
|
-
}
|
|
1100
|
-
return true;
|
|
1101
|
-
}
|
|
1102
|
-
const transform = schema.transformation(field);
|
|
1103
|
-
const rawValue = value.map(item => transform.serialize(item, field.options ?? null, target));
|
|
1104
|
-
cache.setAttr(identifier, propArray, rawValue);
|
|
1105
|
-
const peeked = peekManagedArray(self, field);
|
|
1106
|
-
if (peeked) {
|
|
1107
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1108
|
-
if (!test) {
|
|
1109
|
-
throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
|
|
1110
|
-
}
|
|
1111
|
-
})(ARRAY_SIGNAL in peeked) : {};
|
|
1112
|
-
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1113
|
-
arrSignal.isStale = true;
|
|
1114
|
-
}
|
|
1115
|
-
return true;
|
|
1116
|
-
}
|
|
1117
1711
|
case 'schema-array':
|
|
1118
|
-
{
|
|
1119
|
-
const arrayValue = value?.slice();
|
|
1120
|
-
if (!Array.isArray(arrayValue)) {
|
|
1121
|
-
ManagedArrayMap.delete(target);
|
|
1122
|
-
}
|
|
1123
|
-
cache.setAttr(identifier, propArray, arrayValue);
|
|
1124
|
-
const peeked = peekManagedArray(self, field);
|
|
1125
|
-
if (peeked) {
|
|
1126
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1127
|
-
if (!test) {
|
|
1128
|
-
throw new Error(`Expected the peekManagedArray for ${field.kind} to return a ManagedArray`);
|
|
1129
|
-
}
|
|
1130
|
-
})(ARRAY_SIGNAL in peeked) : {};
|
|
1131
|
-
const arrSignal = peeked[ARRAY_SIGNAL];
|
|
1132
|
-
arrSignal.isStale = true;
|
|
1133
|
-
}
|
|
1134
|
-
if (!Array.isArray(value)) {
|
|
1135
|
-
ManagedArrayMap.delete(target);
|
|
1136
|
-
}
|
|
1137
|
-
return true;
|
|
1138
|
-
}
|
|
1139
|
-
case 'object':
|
|
1140
|
-
{
|
|
1141
|
-
if (!field.type) {
|
|
1142
|
-
let newValue = value;
|
|
1143
|
-
if (value !== null) {
|
|
1144
|
-
newValue = {
|
|
1145
|
-
...value
|
|
1146
|
-
};
|
|
1147
|
-
} else {
|
|
1148
|
-
ManagedObjectMap.delete(target);
|
|
1149
|
-
}
|
|
1150
|
-
cache.setAttr(identifier, propArray, newValue);
|
|
1151
|
-
const peeked = peekManagedObject(self, field);
|
|
1152
|
-
if (peeked) {
|
|
1153
|
-
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1154
|
-
objSignal.isStale = true;
|
|
1155
|
-
}
|
|
1156
|
-
return true;
|
|
1157
|
-
}
|
|
1158
|
-
const transform = schema.transformation(field);
|
|
1159
|
-
const rawValue = transform.serialize({
|
|
1160
|
-
...value
|
|
1161
|
-
}, field.options ?? null, target);
|
|
1162
|
-
cache.setAttr(identifier, propArray, rawValue);
|
|
1163
|
-
const peeked = peekManagedObject(self, field);
|
|
1164
|
-
if (peeked) {
|
|
1165
|
-
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1166
|
-
objSignal.isStale = true;
|
|
1167
|
-
}
|
|
1168
|
-
return true;
|
|
1169
|
-
}
|
|
1170
1712
|
case 'schema-object':
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
if (value !== null) {
|
|
1174
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1175
|
-
if (!test) {
|
|
1176
|
-
throw new Error(`Expected value to be an object`);
|
|
1177
|
-
}
|
|
1178
|
-
})(typeof value === 'object') : {};
|
|
1179
|
-
newValue = {
|
|
1180
|
-
...value
|
|
1181
|
-
};
|
|
1182
|
-
const schemaFields = schema.fields({
|
|
1183
|
-
type: field.type
|
|
1184
|
-
});
|
|
1185
|
-
for (const key of Object.keys(newValue)) {
|
|
1186
|
-
if (!schemaFields.has(key)) {
|
|
1187
|
-
throw new Error(`Field ${key} does not exist on schema object ${field.type}`);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
} else {
|
|
1191
|
-
ManagedObjectMap.delete(target);
|
|
1192
|
-
}
|
|
1193
|
-
cache.setAttr(identifier, propArray, newValue);
|
|
1194
|
-
// const peeked = peekManagedObject(self, field);
|
|
1195
|
-
// if (peeked) {
|
|
1196
|
-
// const objSignal = peeked[OBJECT_SIGNAL];
|
|
1197
|
-
// objSignal.isStale = true;
|
|
1198
|
-
// }
|
|
1199
|
-
return true;
|
|
1200
|
-
}
|
|
1201
|
-
case 'derived':
|
|
1202
|
-
{
|
|
1203
|
-
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because it is derived`);
|
|
1204
|
-
}
|
|
1713
|
+
case 'object':
|
|
1714
|
+
case 'resource':
|
|
1205
1715
|
case 'belongsTo':
|
|
1206
|
-
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1207
|
-
if (!test) {
|
|
1208
|
-
throw new Error(`Can only use belongsTo fields when the resource is in legacy mode`);
|
|
1209
|
-
}
|
|
1210
|
-
})(Mode[Legacy]) : {};
|
|
1211
|
-
schema._kind('@legacy', 'belongsTo').set(store, receiver, identifier, field, value);
|
|
1212
|
-
return true;
|
|
1213
1716
|
case 'hasMany':
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1717
|
+
case 'collection':
|
|
1718
|
+
return DefaultMode[field.kind].set({
|
|
1719
|
+
store,
|
|
1720
|
+
resourceKey: identifier,
|
|
1721
|
+
modeName: context.modeName,
|
|
1722
|
+
legacy: context.legacy,
|
|
1723
|
+
editable: context.editable,
|
|
1724
|
+
path: propArray,
|
|
1725
|
+
field: field,
|
|
1726
|
+
record: receiver,
|
|
1727
|
+
signals,
|
|
1728
|
+
value
|
|
1729
|
+
});
|
|
1226
1730
|
default:
|
|
1227
|
-
|
|
1731
|
+
return assertNeverField(identifier, field, propArray);
|
|
1228
1732
|
}
|
|
1229
1733
|
}
|
|
1230
1734
|
});
|
|
@@ -1255,25 +1759,25 @@ class ReactiveResource {
|
|
|
1255
1759
|
// TODO we should likely handle this notification here
|
|
1256
1760
|
// also we should add a LOGGING flag
|
|
1257
1761
|
// eslint-disable-next-line no-console
|
|
1258
|
-
console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`,
|
|
1762
|
+
console.warn(`Notification unhandled for ${key.join(',')} on ${identifier.type}`, proxy);
|
|
1259
1763
|
return;
|
|
1260
1764
|
}
|
|
1261
1765
|
|
|
1262
1766
|
// TODO we should add a LOGGING flag
|
|
1263
|
-
// console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`,
|
|
1767
|
+
// console.log(`Deep notification skipped for ${key.join('.')} on ${identifier.type}`, proxy);
|
|
1264
1768
|
// deep notify the key path
|
|
1265
1769
|
} else {
|
|
1266
1770
|
if (isEmbedded) return; // base paths never apply to embedded records
|
|
1267
1771
|
|
|
1268
1772
|
// TODO determine what LOGGING flag to wrap this in if any
|
|
1269
|
-
// console.log(`Notification for ${key} on ${identifier.type}`,
|
|
1773
|
+
// console.log(`Notification for ${key} on ${identifier.type}`, proxy);
|
|
1270
1774
|
const signal = signals.get(key);
|
|
1271
1775
|
if (signal) {
|
|
1272
1776
|
notifyInternalSignal(signal);
|
|
1273
1777
|
}
|
|
1274
|
-
const field =
|
|
1778
|
+
const field = cacheFields.get(key);
|
|
1275
1779
|
if (field?.kind === 'array' || field?.kind === 'schema-array') {
|
|
1276
|
-
const peeked =
|
|
1780
|
+
const peeked = signal?.value;
|
|
1277
1781
|
if (peeked) {
|
|
1278
1782
|
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1279
1783
|
if (!test) {
|
|
@@ -1285,7 +1789,7 @@ class ReactiveResource {
|
|
|
1285
1789
|
}
|
|
1286
1790
|
}
|
|
1287
1791
|
if (field?.kind === 'object') {
|
|
1288
|
-
const peeked = peekManagedObject(
|
|
1792
|
+
const peeked = peekManagedObject(proxy, field);
|
|
1289
1793
|
if (peeked) {
|
|
1290
1794
|
const objSignal = peeked[OBJECT_SIGNAL];
|
|
1291
1795
|
notifyInternalSignal(objSignal);
|
|
@@ -1299,15 +1803,15 @@ class ReactiveResource {
|
|
|
1299
1803
|
if (Array.isArray(key)) ;else {
|
|
1300
1804
|
if (isEmbedded) return; // base paths never apply to embedded records
|
|
1301
1805
|
|
|
1302
|
-
const field =
|
|
1806
|
+
const field = cacheFields.get(key);
|
|
1303
1807
|
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1304
1808
|
if (!test) {
|
|
1305
|
-
throw new Error(`Expected
|
|
1809
|
+
throw new Error(`Expected relationship ${key} to be the name of a field`);
|
|
1306
1810
|
}
|
|
1307
1811
|
})(field) : {};
|
|
1308
1812
|
if (field.kind === 'belongsTo') {
|
|
1309
1813
|
// TODO determine what LOGGING flag to wrap this in if any
|
|
1310
|
-
// console.log(`Notification for ${key} on ${identifier.type}`,
|
|
1814
|
+
// console.log(`Notification for ${key} on ${identifier.type}`, proxy);
|
|
1311
1815
|
const signal = signals.get(key);
|
|
1312
1816
|
if (signal) {
|
|
1313
1817
|
notifyInternalSignal(signal);
|
|
@@ -1315,9 +1819,12 @@ class ReactiveResource {
|
|
|
1315
1819
|
// FIXME
|
|
1316
1820
|
} else if (field.kind === 'resource') ;else if (field.kind === 'hasMany') {
|
|
1317
1821
|
if (field.options.linksMode) {
|
|
1318
|
-
const
|
|
1319
|
-
if (
|
|
1320
|
-
|
|
1822
|
+
const signal = signals.get(key);
|
|
1823
|
+
if (signal) {
|
|
1824
|
+
const peeked = signal.value;
|
|
1825
|
+
if (peeked) {
|
|
1826
|
+
notifyInternalSignal(peeked[ARRAY_SIGNAL]);
|
|
1827
|
+
}
|
|
1321
1828
|
}
|
|
1322
1829
|
return;
|
|
1323
1830
|
}
|
|
@@ -1325,7 +1832,7 @@ class ReactiveResource {
|
|
|
1325
1832
|
if (!test) {
|
|
1326
1833
|
throw new Error(`Can only use hasMany fields when the resource is in legacy mode`);
|
|
1327
1834
|
}
|
|
1328
|
-
})(
|
|
1835
|
+
})(context.legacy) : {};
|
|
1329
1836
|
if (schema._kind('@legacy', 'hasMany').notify(store, proxy, identifier, field)) {
|
|
1330
1837
|
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1331
1838
|
if (!test) {
|
|
@@ -1381,10 +1888,17 @@ function _CHECKOUT(record) {
|
|
|
1381
1888
|
if (isEmbedded) {
|
|
1382
1889
|
throw new Error(`Cannot checkout an embedded record (yet)`);
|
|
1383
1890
|
}
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1891
|
+
const legacy = record[Legacy];
|
|
1892
|
+
const editableRecord = new ReactiveResource({
|
|
1893
|
+
store: record[RecordStore],
|
|
1894
|
+
resourceKey: record[Identifier],
|
|
1895
|
+
modeName: legacy ? 'legacy' : 'polaris',
|
|
1896
|
+
legacy: legacy,
|
|
1897
|
+
editable: true,
|
|
1898
|
+
path: null,
|
|
1899
|
+
field: null,
|
|
1900
|
+
value: null
|
|
1901
|
+
});
|
|
1388
1902
|
setRecordIdentifier(editableRecord, recordIdentifierFor(record));
|
|
1389
1903
|
return Promise.resolve(editableRecord);
|
|
1390
1904
|
}
|
|
@@ -1396,6 +1910,17 @@ function _DESTROY(record) {
|
|
|
1396
1910
|
record.isDestroyed = true;
|
|
1397
1911
|
}
|
|
1398
1912
|
record[RecordStore].notifications.unsubscribe(record.___notifications);
|
|
1913
|
+
|
|
1914
|
+
// FIXME we need a way to also unsubscribe all SchemaObjects when the primary
|
|
1915
|
+
// resource is destroyed.
|
|
1916
|
+
}
|
|
1917
|
+
function assertNeverField(identifier, field, path) {
|
|
1918
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1919
|
+
{
|
|
1920
|
+
throw new Error(`Cannot use unknown field kind ${field.kind} on <${identifier.type}>.${Array.isArray(path) ? path.join('.') : path}`);
|
|
1921
|
+
}
|
|
1922
|
+
})() : {};
|
|
1923
|
+
return false;
|
|
1399
1924
|
}
|
|
1400
1925
|
function instantiateRecord(store, identifier, createArgs) {
|
|
1401
1926
|
const schema = store.schema;
|
|
@@ -1405,11 +1930,17 @@ function instantiateRecord(store, identifier, createArgs) {
|
|
|
1405
1930
|
throw new Error(`Expected a resource schema`);
|
|
1406
1931
|
}
|
|
1407
1932
|
})(isResourceSchema(resourceSchema)) : {};
|
|
1408
|
-
const
|
|
1409
|
-
const
|
|
1410
|
-
const record = new ReactiveResource(
|
|
1411
|
-
|
|
1412
|
-
|
|
1933
|
+
const legacy = resourceSchema?.legacy ?? false;
|
|
1934
|
+
const editable = legacy || store.cache.isNew(identifier);
|
|
1935
|
+
const record = new ReactiveResource({
|
|
1936
|
+
store,
|
|
1937
|
+
resourceKey: identifier,
|
|
1938
|
+
modeName: legacy ? 'legacy' : 'polaris',
|
|
1939
|
+
legacy: legacy,
|
|
1940
|
+
editable: editable,
|
|
1941
|
+
path: null,
|
|
1942
|
+
field: null,
|
|
1943
|
+
value: null
|
|
1413
1944
|
});
|
|
1414
1945
|
if (createArgs) {
|
|
1415
1946
|
Object.assign(record, createArgs);
|
|
@@ -1546,7 +2077,7 @@ function getExt(extCache, type, extName) {
|
|
|
1546
2077
|
function hasObjectSchema(field) {
|
|
1547
2078
|
return 'kind' in field && (field.kind === 'schema-array' || field.kind === 'schema-object');
|
|
1548
2079
|
}
|
|
1549
|
-
function processExtensions(schema, field, scenario) {
|
|
2080
|
+
function processExtensions(schema, field, scenario, resolvedType) {
|
|
1550
2081
|
// if we're looking up extensions for a resource, there is no
|
|
1551
2082
|
// merging required so if we have no objectExtensions
|
|
1552
2083
|
// we are done.
|
|
@@ -1573,12 +2104,16 @@ function processExtensions(schema, field, scenario) {
|
|
|
1573
2104
|
if (!hasObjectSchema(field)) {
|
|
1574
2105
|
return null;
|
|
1575
2106
|
}
|
|
1576
|
-
return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(
|
|
2107
|
+
return schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
|
|
2108
|
+
type: resolvedType
|
|
2109
|
+
} : field);
|
|
1577
2110
|
}
|
|
1578
2111
|
|
|
1579
2112
|
// if we have made it here, we have extensions, lets check if there's
|
|
1580
2113
|
// a cached version we can use
|
|
1581
|
-
const baseExtensions = scenario === 'resource' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : scenario === 'object' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(
|
|
2114
|
+
const baseExtensions = scenario === 'resource' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(field) : scenario === 'object' && hasObjectSchema(field) ? schema.CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resolvedType ? {
|
|
2115
|
+
type: resolvedType
|
|
2116
|
+
} : field) : null;
|
|
1582
2117
|
if (!baseExtensions && extensions.length === 1) {
|
|
1583
2118
|
const value = getExt(extCache, type, extensions[0]);
|
|
1584
2119
|
fieldCache[type].set(field, value);
|
|
@@ -1724,7 +2259,7 @@ class SchemaService {
|
|
|
1724
2259
|
this._transforms = new Map();
|
|
1725
2260
|
this._hashFns = new Map();
|
|
1726
2261
|
this._derivations = new Map();
|
|
1727
|
-
this._traits = new
|
|
2262
|
+
this._traits = new Map();
|
|
1728
2263
|
this._modes = new Map();
|
|
1729
2264
|
this._extensions = {
|
|
1730
2265
|
object: new Map(),
|
|
@@ -1821,7 +2356,7 @@ class SchemaService {
|
|
|
1821
2356
|
const fields = new Map();
|
|
1822
2357
|
const relationships = {};
|
|
1823
2358
|
const attributes = {};
|
|
1824
|
-
schema.fields
|
|
2359
|
+
for (const field of schema.fields) {
|
|
1825
2360
|
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
1826
2361
|
if (!test) {
|
|
1827
2362
|
throw new Error(`${field.kind} is not valid inside a ResourceSchema's fields.`);
|
|
@@ -1835,20 +2370,62 @@ class SchemaService {
|
|
|
1835
2370
|
} else if (field.kind === 'belongsTo' || field.kind === 'hasMany') {
|
|
1836
2371
|
relationships[field.name] = field;
|
|
1837
2372
|
}
|
|
1838
|
-
}
|
|
2373
|
+
}
|
|
2374
|
+
const cacheFields = null;
|
|
1839
2375
|
const traits = new Set(isResourceSchema(schema) ? schema.traits : []);
|
|
1840
|
-
traits.
|
|
1841
|
-
this._traits.add(trait);
|
|
1842
|
-
});
|
|
2376
|
+
const finalized = traits.size === 0;
|
|
1843
2377
|
const internalSchema = {
|
|
1844
2378
|
original: schema,
|
|
2379
|
+
finalized,
|
|
1845
2380
|
fields,
|
|
2381
|
+
cacheFields,
|
|
1846
2382
|
relationships,
|
|
1847
2383
|
attributes,
|
|
1848
2384
|
traits
|
|
1849
2385
|
};
|
|
2386
|
+
if (traits.size === 0) {
|
|
2387
|
+
internalSchema.cacheFields = getCacheFields(internalSchema);
|
|
2388
|
+
}
|
|
1850
2389
|
this._schemas.set(schema.type, internalSchema);
|
|
1851
2390
|
}
|
|
2391
|
+
|
|
2392
|
+
/**
|
|
2393
|
+
* Registers a {@link Trait} for use by resource schemas.
|
|
2394
|
+
*
|
|
2395
|
+
* Traits are re-usable collections of fields that can be composed to
|
|
2396
|
+
* build up a resource schema. Often they represent polymorphic behaviors
|
|
2397
|
+
* a resource should exhibit.
|
|
2398
|
+
*
|
|
2399
|
+
* When we finalize a resource, we walk its traits and apply their fields
|
|
2400
|
+
* to the resource's fields. All specified traits must be registered by
|
|
2401
|
+
* this time or an error will be thrown.
|
|
2402
|
+
*
|
|
2403
|
+
* Traits are applied left-to-right, with traits of traits being applied in the same
|
|
2404
|
+
* way. Thus for the most part, application of traits is a post-order graph traversal
|
|
2405
|
+
* problem.
|
|
2406
|
+
*
|
|
2407
|
+
* A trait is only ever processed once. If multiple traits (A, B, C) have the same
|
|
2408
|
+
* trait (D) as a dependency, D will be included only once when first encountered by
|
|
2409
|
+
* A.
|
|
2410
|
+
*
|
|
2411
|
+
* If a cycle exists such that trait A has trait B which has Trait A, trait A will
|
|
2412
|
+
* be applied *after* trait B in production. In development a cycle error will be thrown.
|
|
2413
|
+
*
|
|
2414
|
+
* Fields are finalized on a "last wins principle". Thus traits appearing higher in
|
|
2415
|
+
* the tree and further to the right of a traits array take precedence, with the
|
|
2416
|
+
* resource's fields always being applied last and winning out.
|
|
2417
|
+
*
|
|
2418
|
+
* @public
|
|
2419
|
+
*/
|
|
2420
|
+
registerTrait(trait) {
|
|
2421
|
+
const internalTrait = Object.assign({}, trait, {
|
|
2422
|
+
fields: new Map()
|
|
2423
|
+
});
|
|
2424
|
+
for (const field of trait.fields) {
|
|
2425
|
+
internalTrait.fields.set(field.name, field);
|
|
2426
|
+
}
|
|
2427
|
+
this._traits.set(trait.name, internalTrait);
|
|
2428
|
+
}
|
|
1852
2429
|
registerTransformation(transformation) {
|
|
1853
2430
|
this._transforms.set(transformation[Type], transformation);
|
|
1854
2431
|
}
|
|
@@ -1865,13 +2442,16 @@ class SchemaService {
|
|
|
1865
2442
|
}
|
|
1866
2443
|
CAUTION_MEGA_DANGER_ZONE_resourceExtensions(resource) {
|
|
1867
2444
|
const schema = this.resource(resource);
|
|
1868
|
-
return processExtensions(this, schema, 'resource');
|
|
2445
|
+
return processExtensions(this, schema, 'resource', null);
|
|
1869
2446
|
}
|
|
1870
|
-
CAUTION_MEGA_DANGER_ZONE_objectExtensions(field) {
|
|
1871
|
-
return processExtensions(this, field, 'object');
|
|
2447
|
+
CAUTION_MEGA_DANGER_ZONE_objectExtensions(field, resolvedType) {
|
|
2448
|
+
return processExtensions(this, field, 'object', resolvedType);
|
|
1872
2449
|
}
|
|
1873
2450
|
CAUTION_MEGA_DANGER_ZONE_arrayExtensions(field) {
|
|
1874
|
-
return processExtensions(this, field, 'array');
|
|
2451
|
+
return processExtensions(this, field, 'array', null);
|
|
2452
|
+
}
|
|
2453
|
+
CAUTION_MEGA_DANGER_ZONE_hasExtension(ext) {
|
|
2454
|
+
return this._extensions[ext.kind].has(ext.name);
|
|
1875
2455
|
}
|
|
1876
2456
|
|
|
1877
2457
|
/**
|
|
@@ -1926,11 +2506,30 @@ class SchemaService {
|
|
|
1926
2506
|
type
|
|
1927
2507
|
}) {
|
|
1928
2508
|
const schema = this._schemas.get(type);
|
|
1929
|
-
|
|
1930
|
-
|
|
2509
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
2510
|
+
if (!test) {
|
|
2511
|
+
throw new Error(`No schema defined for ${type}`);
|
|
2512
|
+
}
|
|
2513
|
+
})(schema) : {};
|
|
2514
|
+
if (!schema.finalized) {
|
|
2515
|
+
finalizeResource(this, schema);
|
|
1931
2516
|
}
|
|
1932
2517
|
return schema.fields;
|
|
1933
2518
|
}
|
|
2519
|
+
cacheFields({
|
|
2520
|
+
type
|
|
2521
|
+
}) {
|
|
2522
|
+
const schema = this._schemas.get(type);
|
|
2523
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
2524
|
+
if (!test) {
|
|
2525
|
+
throw new Error(`No schema defined for ${type}`);
|
|
2526
|
+
}
|
|
2527
|
+
})(schema) : {};
|
|
2528
|
+
if (!schema.finalized) {
|
|
2529
|
+
finalizeResource(this, schema);
|
|
2530
|
+
}
|
|
2531
|
+
return schema.cacheFields;
|
|
2532
|
+
}
|
|
1934
2533
|
hasResource(resource) {
|
|
1935
2534
|
return this._schemas.has(resource.type);
|
|
1936
2535
|
}
|
|
@@ -1985,4 +2584,103 @@ if (macroCondition(getGlobalConfig().WarpDrive.deprecations.ENABLE_LEGACY_SCHEMA
|
|
|
1985
2584
|
return this._schemas.has(type);
|
|
1986
2585
|
};
|
|
1987
2586
|
}
|
|
2587
|
+
|
|
2588
|
+
/**
|
|
2589
|
+
* When we finalize a resource, we walk its traits and apply their fields
|
|
2590
|
+
* to the resource's fields.
|
|
2591
|
+
*
|
|
2592
|
+
* Traits are applied left-to-right, with traits of traits being applied in the same
|
|
2593
|
+
* way. Thus for the most part, application of traits is a post-order graph traversal
|
|
2594
|
+
* problem.
|
|
2595
|
+
*
|
|
2596
|
+
* A trait is only ever processed once. If multiple traits (A, B, C) have the same
|
|
2597
|
+
* trait (D) as a dependency, D will be included only once when first encountered by
|
|
2598
|
+
* A.
|
|
2599
|
+
*
|
|
2600
|
+
* If a cycle exists such that trait A has trait B which has Trait A, trait A will
|
|
2601
|
+
* be applied *after* trait B in production. In development a cycle error will be thrown.
|
|
2602
|
+
*
|
|
2603
|
+
* Fields are finalized on a "last wins principle". Thus traits appearing higher in
|
|
2604
|
+
* the tree and further to the right of a traits array take precedence, with the
|
|
2605
|
+
* resource's fields always being applied last and winning out.
|
|
2606
|
+
*/
|
|
2607
|
+
function finalizeResource(schema, resource) {
|
|
2608
|
+
const fields = new Map();
|
|
2609
|
+
const seen = new Set();
|
|
2610
|
+
for (const traitName of resource.traits) {
|
|
2611
|
+
const trait = schema._traits.get(traitName);
|
|
2612
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
2613
|
+
if (!test) {
|
|
2614
|
+
throw new Error(`The trait ${traitName} MUST be supplied before the resource ${resource.original.type} can be finalized for use.`);
|
|
2615
|
+
}
|
|
2616
|
+
})(trait) : {};
|
|
2617
|
+
walkTrait(schema, trait, fields, seen, resource.original.type, macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? [] : null);
|
|
2618
|
+
}
|
|
2619
|
+
mergeMap(fields, resource.fields);
|
|
2620
|
+
resource.fields = fields;
|
|
2621
|
+
resource.cacheFields = getCacheFields(resource);
|
|
2622
|
+
resource.finalized = true;
|
|
2623
|
+
}
|
|
2624
|
+
function getCacheFields(resource) {
|
|
2625
|
+
const {
|
|
2626
|
+
fields
|
|
2627
|
+
} = resource;
|
|
2628
|
+
const cacheFields = new Map();
|
|
2629
|
+
for (const [key, value] of fields) {
|
|
2630
|
+
if (isNonIdentityCacheableField(value)) {
|
|
2631
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
2632
|
+
if (!test) {
|
|
2633
|
+
throw new Error(`The sourceKey '${value.sourceKey}' for the field '${key}' on ${resource.original.type} is invalid because it matches the name of an existing field`);
|
|
2634
|
+
}
|
|
2635
|
+
})(!value.sourceKey || value.sourceKey === key || !fields.has(value.sourceKey)) : {};
|
|
2636
|
+
const cacheKey = getFieldCacheKeyStrict(value);
|
|
2637
|
+
cacheFields.set(cacheKey, value);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return cacheFields;
|
|
2641
|
+
}
|
|
2642
|
+
function walkTrait(schema, trait, fields, seen, type, debugPath) {
|
|
2643
|
+
if (seen.has(trait)) {
|
|
2644
|
+
// if the trait is in the current path, we throw a cycle error in dev.
|
|
2645
|
+
if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
|
|
2646
|
+
if (debugPath.includes(trait.name)) {
|
|
2647
|
+
throw new Error(`CycleError: The Trait '${trait.name}' utilized by the Resource '${type}' includes the following circular reference "${debugPath.join(' > ')} > ${trait.name}"`);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
const ownPath = macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? [...debugPath, trait.name] : null;
|
|
2653
|
+
|
|
2654
|
+
// immediately mark as seen to prevent cycles
|
|
2655
|
+
// further down the tree from looping back
|
|
2656
|
+
seen.add(trait);
|
|
2657
|
+
|
|
2658
|
+
// first apply any child traits
|
|
2659
|
+
if (trait.traits?.length) {
|
|
2660
|
+
for (const traitName of trait.traits) {
|
|
2661
|
+
const subtrait = schema._traits.get(traitName);
|
|
2662
|
+
if (macroCondition(getGlobalConfig().WarpDrive.features.ENFORCE_STRICT_RESOURCE_FINALIZATION)) {
|
|
2663
|
+
macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
|
|
2664
|
+
if (!test) {
|
|
2665
|
+
throw new Error(`The trait ${traitName} used by the trait ${trait.name} MUST be supplied before the resource ${type} can be finalized for use.`);
|
|
2666
|
+
}
|
|
2667
|
+
})(subtrait) : {};
|
|
2668
|
+
} else {
|
|
2669
|
+
warn(`The trait ${traitName} used by the trait ${trait.name} MUST be supplied before the resource ${type} can be finalized for use.`, !!subtrait, {
|
|
2670
|
+
id: 'warp-drive:missing-trait-schema-for-resource'
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
if (!subtrait) continue;
|
|
2674
|
+
walkTrait(schema, subtrait, fields, seen, type, ownPath);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// then apply our own fields
|
|
2679
|
+
mergeMap(fields, trait.fields);
|
|
2680
|
+
}
|
|
2681
|
+
function mergeMap(base, toApply) {
|
|
2682
|
+
for (const [key, value] of toApply) {
|
|
2683
|
+
base.set(key, value);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
1988
2686
|
export { Checkout, SchemaService, fromIdentity, instantiateRecord, registerDerivations, teardownRecord, withDefaults };
|