@warp-drive/schema-record 4.13.0-alpha.5 → 4.13.0-alpha.7

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/README.md CHANGED
@@ -1,23 +1,21 @@
1
1
  <p align="center">
2
2
  <img
3
3
  class="project-logo"
4
- src="./logos/NCC-1701-a-blue.svg#gh-light-mode-only"
5
- alt="WarpDrive"
6
- width="120px"
7
- title="WarpDrive" />
4
+ src="./logos/github-header.svg#gh-light-mode-only"
5
+ alt="WarpDrive | Boldly go where no app has gone before"
6
+ title="WarpDrive | Boldly go where no app has gone before"
7
+ />
8
8
  <img
9
9
  class="project-logo"
10
- src="./logos/NCC-1701-a.svg#gh-dark-mode-only"
11
- alt="WarpDrive"
12
- width="120px"
13
- title="WarpDrive" />
10
+ src="./logos/github-header.svg#gh-dark-mode-only"
11
+ alt="WarpDrive | Boldly go where no app has gone before"
12
+ title="WarpDrive | Boldly go where no app has gone before"
13
+ />
14
14
  </p>
15
15
 
16
16
  <h3 align="center">Your Data, Managed.</h3>
17
17
  <p align="center">🌲 Get back to Nature 🐿️ Or shipping 💚</p>
18
18
 
19
- SchemaRecord is:
20
-
21
19
  - ⚡️ Fast
22
20
  - 📦 Tiny
23
21
  - ✨ Optimized
@@ -25,11 +23,15 @@ SchemaRecord is:
25
23
  - ⚛️ Universal
26
24
  - ☢️ Reactive
27
25
 
28
- This package provides reactive capabilities for your resource data.
29
- It works together with a [*Warp***Drive**](https://github.com/emberjs/data/)
30
- [Cache](https://github.com/emberjs/data/blob/main/packages/core-types/src/cache.ts)
31
- and associated Schemas to simplify the most complex parts of your
32
- app's state management.
26
+ SchemaRecord is a reactive object that transforms raw data from an [associated cache](https://github.com/emberjs/data/blob/main/packages/core-types/src/cache.ts) into reactive data backed by Signals.
27
+
28
+ The shape of the object and the transformation of raw cache data into its
29
+ reactive form is controlled by a resource schema.
30
+
31
+ Resource schemas are simple JSON, allowing them to be defined and delivered from anywhere.
32
+
33
+ The capabilities that SchemaRecord brings to [*Warp***Drive**](https://github.com/emberjs/data/)
34
+ will simplify even the most complex parts of your app's state management.
33
35
 
34
36
  ## Installation
35
37
 
@@ -150,7 +152,8 @@ store.registerSchemas([
150
152
  options: {
151
153
  async: false,
152
154
  inverse: 'owner',
153
- polymorphic: true
155
+ polymorphic: true,
156
+ linksMode: true,
154
157
  }
155
158
  }
156
159
  ]
@@ -174,6 +177,7 @@ store.registerSchemas([
174
177
  async: false,
175
178
  inverse: 'pets',
176
179
  as: 'pet',
180
+ linksMode: true,
177
181
  }
178
182
  }
179
183
  ]
@@ -286,6 +290,16 @@ store.registerSchemas([
286
290
  ]);
287
291
  ```
288
292
 
293
+ Additionally, `@warp-drive/core-types` provides several utilities for type-checking and narrowing schemas.
294
+
295
+ - (type) [PolarisResourceSchema]()
296
+ - (type) [LegacyResourceSchema]()
297
+ - (type) [ObjectSchema]()
298
+ - [resourceSchema]()
299
+ - [objectSchema]()
300
+ - [isResourceSchema]()
301
+ - [isLegacyResourceSchema]()
302
+
289
303
 
290
304
  ### Field Schemas
291
305
 
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isResourceSchema } from '@warp-drive/core-types/schema/fields';
1
2
  import { macroCondition, getGlobalConfig, dependencySatisfies, importSync } from '@embroider/macros';
2
3
  import { SOURCE as SOURCE$1, fastPush, RelatedCollection, setRecordIdentifier, recordIdentifierFor } from '@ember-data/store/-private';
3
4
  import { createSignal, subscribe, defineSignal, peekSignal, getSignal, Signals, addToTransaction, entangleSignal } from '@ember-data/tracking/-private';
@@ -307,7 +308,7 @@ class ManagedObject {
307
308
  return self[prop];
308
309
  }
309
310
  if (prop === Symbol.toPrimitive) {
310
- return null;
311
+ return () => null;
311
312
  }
312
313
  if (prop === Symbol.toStringTag) {
313
314
  return `ManagedObject<${identifier.type}:${identifier.id} (${identifier.lid})>`;
@@ -322,7 +323,12 @@ class ManagedObject {
322
323
  }
323
324
  if (prop === 'toHTML') {
324
325
  return function () {
325
- return '<div>ManagedObject</div>';
326
+ return '<span>ManagedObject</span>';
327
+ };
328
+ }
329
+ if (prop === 'toJSON') {
330
+ return function () {
331
+ return structuredClone(self[SOURCE]);
326
332
  };
327
333
  }
328
334
  if (_SIGNAL.shouldReset) {
@@ -661,6 +667,9 @@ const RecordSymbols = new Set(symbolList);
661
667
  function isPathMatch(a, b) {
662
668
  return a.length === b.length && a.every((v, i) => v === b[i]);
663
669
  }
670
+ function isNonEnumerableProp(prop) {
671
+ return prop === 'constructor' || prop === 'prototype' || prop === '__proto__' || prop === 'toString' || prop === 'toJSON' || prop === 'toHTML' || typeof prop === 'symbol';
672
+ }
664
673
  const Editables = new WeakMap();
665
674
  class SchemaRecord {
666
675
  constructor(store, identifier, Mode, isEmbedded = false, embeddedType = null, embeddedPath = null) {
@@ -676,22 +685,25 @@ class SchemaRecord {
676
685
  this[Legacy] = Mode[Legacy] ?? false;
677
686
  const schema = store.schema;
678
687
  const cache = store.cache;
679
- const identityField = schema.resource(identifier).identity;
688
+ const identityField = schema.resource(isEmbedded ? {
689
+ type: embeddedType
690
+ } : identifier).identity;
691
+ const BoundFns = new Map();
680
692
  this[EmbeddedType] = embeddedType;
681
693
  this[EmbeddedPath] = embeddedPath;
682
- let fields;
683
- if (isEmbedded) {
684
- fields = schema.fields({
685
- type: embeddedType
686
- });
687
- } else {
688
- fields = schema.fields(identifier);
689
- }
694
+ const fields = isEmbedded ? schema.fields({
695
+ type: embeddedType
696
+ }) : schema.fields(identifier);
690
697
  const signals = new Map();
691
698
  this[Signals] = signals;
692
699
  const proxy = new Proxy(this, {
693
700
  ownKeys() {
694
- return Array.from(fields.keys());
701
+ const identityKey = identityField?.name;
702
+ const keys = Array.from(fields.keys());
703
+ if (identityKey) {
704
+ keys.unshift(identityKey);
705
+ }
706
+ return keys;
695
707
  },
696
708
  has(target, prop) {
697
709
  if (prop === Destroy || prop === Checkout) {
@@ -700,10 +712,19 @@ class SchemaRecord {
700
712
  return fields.has(prop);
701
713
  },
702
714
  getOwnPropertyDescriptor(target, prop) {
703
- if (!fields.has(prop)) {
704
- throw new Error(`No field named ${String(prop)} on ${identifier.type}`);
715
+ const schemaForField = prop === identityField?.name ? identityField : fields.get(prop);
716
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
717
+ if (!test) {
718
+ throw new Error(`No field named ${String(prop)} on ${identifier.type}`);
719
+ }
720
+ })(schemaForField) : {};
721
+ if (isNonEnumerableProp(prop)) {
722
+ return {
723
+ writable: false,
724
+ enumerable: false,
725
+ configurable: true
726
+ };
705
727
  }
706
- const schemaForField = fields.get(prop);
707
728
  switch (schemaForField.kind) {
708
729
  case 'derived':
709
730
  return {
@@ -711,6 +732,12 @@ class SchemaRecord {
711
732
  enumerable: true,
712
733
  configurable: true
713
734
  };
735
+ case '@id':
736
+ return {
737
+ writable: identifier.id === null,
738
+ enumerable: true,
739
+ configurable: true
740
+ };
714
741
  case '@local':
715
742
  case 'field':
716
743
  case 'attribute':
@@ -728,28 +755,18 @@ class SchemaRecord {
728
755
  enumerable: true,
729
756
  configurable: true
730
757
  };
758
+ default:
759
+ return {
760
+ writable: false,
761
+ enumerable: false,
762
+ configurable: false
763
+ };
731
764
  }
732
765
  },
733
766
  get(target, prop, receiver) {
734
767
  if (RecordSymbols.has(prop)) {
735
768
  return target[prop];
736
769
  }
737
- if (prop === Symbol.toStringTag) {
738
- return `SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})>`;
739
- }
740
- if (prop === 'toString') {
741
- return function () {
742
- return `SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})>`;
743
- };
744
- }
745
- if (prop === 'toHTML') {
746
- return function () {
747
- return `<div>SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})></div>`;
748
- };
749
- }
750
- if (prop === Symbol.toPrimitive) {
751
- return null;
752
- }
753
770
 
754
771
  // TODO make this a symbol
755
772
  if (prop === '___notifications') {
@@ -765,18 +782,75 @@ class SchemaRecord {
765
782
  if (IgnoredGlobalFields.has(prop)) {
766
783
  return undefined;
767
784
  }
785
+
786
+ /////////////////////////////////////////////////////////////
787
+ //// Note these bound function behaviors are essentially ////
788
+ //// built-in but overrideable derivations. ////
789
+ //// ////
790
+ //// The bar for this has to be "basic expectations of ////
791
+ /// an object" – very, very high ////
792
+ /////////////////////////////////////////////////////////////
793
+
794
+ if (prop === Symbol.toStringTag || prop === 'toString') {
795
+ let fn = BoundFns.get('toString');
796
+ if (!fn) {
797
+ fn = function () {
798
+ entangleSignal(signals, receiver, '@identity');
799
+ return `Record<${identifier.type}:${identifier.id} (${identifier.lid})>`;
800
+ };
801
+ BoundFns.set(prop, fn);
802
+ }
803
+ return fn;
804
+ }
805
+ if (prop === 'toHTML') {
806
+ let fn = BoundFns.get('toHTML');
807
+ if (!fn) {
808
+ fn = function () {
809
+ entangleSignal(signals, receiver, '@identity');
810
+ return `<span>Record<${identifier.type}:${identifier.id} (${identifier.lid})></span>`;
811
+ };
812
+ BoundFns.set(prop, fn);
813
+ }
814
+ return fn;
815
+ }
816
+ if (prop === 'toJSON') {
817
+ let fn = BoundFns.get('toJSON');
818
+ if (!fn) {
819
+ fn = function () {
820
+ const json = {};
821
+ for (const key in receiver) {
822
+ json[key] = receiver[key];
823
+ }
824
+ return json;
825
+ };
826
+ BoundFns.set(prop, fn);
827
+ }
828
+ return fn;
829
+ }
830
+ if (prop === Symbol.toPrimitive) return () => null;
831
+ if (prop === Symbol.iterator) {
832
+ let fn = BoundFns.get(Symbol.iterator);
833
+ if (!fn) {
834
+ fn = function* () {
835
+ for (const key in receiver) {
836
+ yield [key, receiver[key]];
837
+ }
838
+ };
839
+ BoundFns.set(Symbol.iterator, fn);
840
+ }
841
+ return fn;
842
+ }
768
843
  if (prop === 'constructor') {
769
844
  return SchemaRecord;
770
845
  }
771
846
  // too many things check for random symbols
772
- if (typeof prop === 'symbol') {
773
- return undefined;
774
- }
775
- let type = identifier.type;
776
- if (isEmbedded) {
777
- type = embeddedType;
778
- }
779
- throw new Error(`No field named ${String(prop)} on ${type}`);
847
+ if (typeof prop === 'symbol') return undefined;
848
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
849
+ {
850
+ throw new Error(`No field named ${String(prop)} on ${isEmbedded ? embeddedType : identifier.type}`);
851
+ }
852
+ })() : {};
853
+ return undefined;
780
854
  }
781
855
  const field = maybeField.kind === 'alias' ? maybeField.options : maybeField;
782
856
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
@@ -804,49 +878,24 @@ class SchemaRecord {
804
878
  return lastValue;
805
879
  }
806
880
  case 'field':
807
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
808
- if (!test) {
809
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
810
- }
811
- })(!target[Legacy]) : {};
812
881
  entangleSignal(signals, receiver, field.name);
813
882
  return computeField(schema, cache, target, identifier, field, propArray, IS_EDITABLE);
814
883
  case 'attribute':
815
884
  entangleSignal(signals, receiver, field.name);
816
885
  return computeAttribute(cache, identifier, prop, IS_EDITABLE);
817
886
  case 'resource':
818
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
819
- if (!test) {
820
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
821
- }
822
- })(!target[Legacy]) : {};
823
887
  entangleSignal(signals, receiver, field.name);
824
888
  return computeResource(store, cache, target, identifier, field, prop, IS_EDITABLE);
825
889
  case 'derived':
826
890
  return computeDerivation(schema, receiver, identifier, field, prop);
827
891
  case 'schema-array':
828
892
  case 'array':
829
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
830
- if (!test) {
831
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
832
- }
833
- })(!target[Legacy]) : {};
834
893
  entangleSignal(signals, receiver, field.name);
835
894
  return computeArray(store, schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
836
895
  case 'object':
837
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
838
- if (!test) {
839
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
840
- }
841
- })(!target[Legacy]) : {};
842
896
  entangleSignal(signals, receiver, field.name);
843
897
  return computeObject(schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
844
898
  case 'schema-object':
845
- macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
846
- if (!test) {
847
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
848
- }
849
- })(!target[Legacy]) : {};
850
899
  entangleSignal(signals, receiver, field.name);
851
900
  // run transform, then use that value as the object to manage
852
901
  return computeSchemaObject(store, cache, target, identifier, field, propArray, Mode[Legacy], Mode[Editable]);
@@ -1292,6 +1341,19 @@ class SchemaRecord {
1292
1341
  break;
1293
1342
  }
1294
1343
  });
1344
+ if (macroCondition(getGlobalConfig().WarpDrive.env.DEBUG)) {
1345
+ Object.defineProperty(this, '__SHOW_ME_THE_DATA_(debug mode only)__', {
1346
+ enumerable: false,
1347
+ configurable: true,
1348
+ get() {
1349
+ const data = {};
1350
+ for (const key of fields.keys()) {
1351
+ data[key] = proxy[key];
1352
+ }
1353
+ return data;
1354
+ }
1355
+ });
1356
+ }
1295
1357
  return proxy;
1296
1358
  }
1297
1359
  [Destroy]() {
@@ -1328,7 +1390,13 @@ class SchemaRecord {
1328
1390
  }
1329
1391
  function instantiateRecord(store, identifier, createArgs) {
1330
1392
  const schema = store.schema;
1331
- const isLegacy = schema.resource(identifier)?.legacy ?? false;
1393
+ const resourceSchema = schema.resource(identifier);
1394
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1395
+ if (!test) {
1396
+ throw new Error(`Expected a resource schema`);
1397
+ }
1398
+ })(isResourceSchema(resourceSchema)) : {};
1399
+ const isLegacy = resourceSchema?.legacy ?? false;
1332
1400
  const isEditable = isLegacy || store.cache.isNew(identifier);
1333
1401
  const record = new SchemaRecord(store, identifier, {
1334
1402
  [Editable]: isEditable,
@@ -1350,19 +1418,28 @@ function teardownRecord(record) {
1350
1418
  assertSchemaRecord(record);
1351
1419
  record[Destroy]();
1352
1420
  }
1421
+
1422
+ /**
1423
+ * @module @warp-drive/schema-record
1424
+ */
1353
1425
  const Support = getOrSetGlobal('Support', new WeakMap());
1354
- const SchemaRecordFields = [{
1426
+ const ConstructorField = {
1355
1427
  type: '@constructor',
1356
1428
  name: 'constructor',
1357
1429
  kind: 'derived'
1358
- }, {
1430
+ };
1431
+ const TypeField = {
1359
1432
  type: '@identity',
1360
1433
  name: '$type',
1361
1434
  kind: 'derived',
1362
1435
  options: {
1363
1436
  key: 'type'
1364
1437
  }
1365
- }];
1438
+ };
1439
+ const DefaultIdentityField = {
1440
+ name: 'id',
1441
+ kind: '@id'
1442
+ };
1366
1443
  function _constructor(record) {
1367
1444
  let state = Support.get(record);
1368
1445
  if (!state) {
@@ -1372,19 +1449,64 @@ function _constructor(record) {
1372
1449
  return state._constructor = state._constructor || {
1373
1450
  name: `SchemaRecord<${recordIdentifierFor$1(record).type}>`,
1374
1451
  get modelName() {
1375
- throw new Error('Cannot access record.constructor.modelName on non-Legacy Schema Records.');
1452
+ macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
1453
+ {
1454
+ throw new Error(`record.constructor.modelName is not available outside of legacy mode`);
1455
+ }
1456
+ })() : {};
1457
+ return undefined;
1376
1458
  }
1377
1459
  };
1378
1460
  }
1379
1461
  _constructor[Type] = '@constructor';
1462
+
1463
+ /**
1464
+ * Utility for constructing a ResourceSchema with the recommended fields
1465
+ * for the Polaris experience.
1466
+ *
1467
+ * @method withDefaults
1468
+ * @for @warp-drive/schema-record
1469
+ * @static
1470
+ * @public
1471
+ * @param schema
1472
+ * @return {ResourceSchema}
1473
+ */
1380
1474
  function withDefaults(schema) {
1381
- schema.identity = schema.identity || {
1382
- name: 'id',
1383
- kind: '@id'
1384
- };
1385
- schema.fields.push(...SchemaRecordFields);
1475
+ schema.identity = schema.identity || DefaultIdentityField;
1476
+
1477
+ // because fields gets iterated in definition order,
1478
+ // we add TypeField to the beginning so that it will
1479
+ // appear right next to the identity field
1480
+ schema.fields.unshift(TypeField);
1481
+ schema.fields.push(ConstructorField);
1386
1482
  return schema;
1387
1483
  }
1484
+
1485
+ /**
1486
+ * A derivation that computes its value from the
1487
+ * record's identity.
1488
+ *
1489
+ * It can be used via a derived field definition like:
1490
+ *
1491
+ * ```ts
1492
+ * {
1493
+ * kind: 'derived',
1494
+ * name: 'id',
1495
+ * type: '@identity',
1496
+ * options: { key: 'id' }
1497
+ * }
1498
+ * ```
1499
+ *
1500
+ * Valid keys are `'id'`, `'lid'`, `'type'`, and `'^'`.
1501
+ *
1502
+ * `^` returns the entire identifier object.
1503
+ *
1504
+ * @method fromIdentity
1505
+ * @for @warp-drive/schema-record
1506
+ * @static
1507
+ * @public
1508
+ */
1509
+
1388
1510
  function fromIdentity(record, options, key) {
1389
1511
  const identifier = record[Identifier];
1390
1512
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
@@ -1400,6 +1522,16 @@ function fromIdentity(record, options, key) {
1400
1522
  return options.key === '^' ? identifier : identifier[options.key];
1401
1523
  }
1402
1524
  fromIdentity[Type] = '@identity';
1525
+
1526
+ /**
1527
+ * Registers the default derivations for the SchemaRecord
1528
+ *
1529
+ * @method registerDerivations
1530
+ * @for @warp-drive/schema-record
1531
+ * @static
1532
+ * @public
1533
+ * @param {SchemaService} schema
1534
+ */
1403
1535
  function registerDerivations(schema) {
1404
1536
  schema.registerDerivation(fromIdentity);
1405
1537
  schema.registerDerivation(_constructor);
@@ -1408,9 +1540,7 @@ function registerDerivations(schema) {
1408
1540
  * Wraps a derivation in a new function with Derivation signature but that looks
1409
1541
  * up the value in the cache before recomputing.
1410
1542
  *
1411
- * @param record
1412
- * @param options
1413
- * @param prop
1543
+ * @internal
1414
1544
  */
1415
1545
  function makeCachedDerivation(derivation) {
1416
1546
  const memoizedDerivation = (record, options, prop) => {
@@ -1427,6 +1557,12 @@ function makeCachedDerivation(derivation) {
1427
1557
  memoizedDerivation[Type] = derivation[Type];
1428
1558
  return memoizedDerivation;
1429
1559
  }
1560
+ /**
1561
+ * A SchemaService designed to work with dynamically registered schemas.
1562
+ *
1563
+ * @class SchemaService
1564
+ * @public
1565
+ */
1430
1566
  class SchemaService {
1431
1567
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1432
1568
 
@@ -1534,7 +1670,7 @@ class SchemaService {
1534
1670
  relationships[field.name] = field;
1535
1671
  }
1536
1672
  });
1537
- const traits = new Set(schema.traits);
1673
+ const traits = new Set(isResourceSchema(schema) ? schema.traits : []);
1538
1674
  traits.forEach(trait => {
1539
1675
  this._traits.add(trait);
1540
1676
  });