@warp-drive-mirror/schema-record 0.0.0-beta.17 → 0.0.0-beta.19

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
 
@@ -124,7 +126,7 @@ We could describe the `'user'` and `'dog'` resources in the above payload
124
126
  with the following schemas:
125
127
 
126
128
  ```ts
127
- store.registerSchemas([
129
+ store.schema.registerResources([
128
130
  {
129
131
  type: 'user',
130
132
  identity: { type: '@id', name: 'id' },
@@ -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
  ]
@@ -243,7 +247,7 @@ definition above using this utility like so:
243
247
  ```ts
244
248
  import { withDefaults } from '@warp-drive-mirror/schema-record';
245
249
 
246
- store.registerSchemas([
250
+ store.schema.registerResources([
247
251
  withDefaults({
248
252
  type: 'user',
249
253
  fields: [
@@ -286,6 +290,16 @@ store.registerSchemas([
286
290
  ]);
287
291
  ```
288
292
 
293
+ Additionally, `@warp-drive-mirror/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
@@ -308,7 +308,7 @@ class ManagedObject {
308
308
  return self[prop];
309
309
  }
310
310
  if (prop === Symbol.toPrimitive) {
311
- return null;
311
+ return () => null;
312
312
  }
313
313
  if (prop === Symbol.toStringTag) {
314
314
  return `ManagedObject<${identifier.type}:${identifier.id} (${identifier.lid})>`;
@@ -323,7 +323,12 @@ class ManagedObject {
323
323
  }
324
324
  if (prop === 'toHTML') {
325
325
  return function () {
326
- return '<div>ManagedObject</div>';
326
+ return '<span>ManagedObject</span>';
327
+ };
328
+ }
329
+ if (prop === 'toJSON') {
330
+ return function () {
331
+ return structuredClone(self[SOURCE]);
327
332
  };
328
333
  }
329
334
  if (_SIGNAL.shouldReset) {
@@ -385,8 +390,14 @@ class ManyArrayManager {
385
390
  array.links = rawValue.links;
386
391
  }
387
392
  const currentState = array[SOURCE$1];
388
- currentState.length = 0;
389
- fastPush(currentState, rawValue.data);
393
+
394
+ // unlike in the normal RecordArray case, we don't need to divorce the reference
395
+ // because we don't need to worry about associate/disassociate since the graph
396
+ // takes care of that for us
397
+ if (currentState !== rawValue.data) {
398
+ currentState.length = 0;
399
+ fastPush(currentState, rawValue.data);
400
+ }
390
401
  }
391
402
  reloadHasMany(key, options) {
392
403
  const field = this.store.schema.fields(this.identifier).get(key);
@@ -662,6 +673,9 @@ const RecordSymbols = new Set(symbolList);
662
673
  function isPathMatch(a, b) {
663
674
  return a.length === b.length && a.every((v, i) => v === b[i]);
664
675
  }
676
+ function isNonEnumerableProp(prop) {
677
+ return prop === 'constructor' || prop === 'prototype' || prop === '__proto__' || prop === 'toString' || prop === 'toJSON' || prop === 'toHTML' || typeof prop === 'symbol';
678
+ }
665
679
  const Editables = new WeakMap();
666
680
  class SchemaRecord {
667
681
  constructor(store, identifier, Mode, isEmbedded = false, embeddedType = null, embeddedPath = null) {
@@ -677,22 +691,25 @@ class SchemaRecord {
677
691
  this[Legacy] = Mode[Legacy] ?? false;
678
692
  const schema = store.schema;
679
693
  const cache = store.cache;
680
- const identityField = schema.resource(identifier).identity;
694
+ const identityField = schema.resource(isEmbedded ? {
695
+ type: embeddedType
696
+ } : identifier).identity;
697
+ const BoundFns = new Map();
681
698
  this[EmbeddedType] = embeddedType;
682
699
  this[EmbeddedPath] = embeddedPath;
683
- let fields;
684
- if (isEmbedded) {
685
- fields = schema.fields({
686
- type: embeddedType
687
- });
688
- } else {
689
- fields = schema.fields(identifier);
690
- }
700
+ const fields = isEmbedded ? schema.fields({
701
+ type: embeddedType
702
+ }) : schema.fields(identifier);
691
703
  const signals = new Map();
692
704
  this[Signals] = signals;
693
705
  const proxy = new Proxy(this, {
694
706
  ownKeys() {
695
- return Array.from(fields.keys());
707
+ const identityKey = identityField?.name;
708
+ const keys = Array.from(fields.keys());
709
+ if (identityKey) {
710
+ keys.unshift(identityKey);
711
+ }
712
+ return keys;
696
713
  },
697
714
  has(target, prop) {
698
715
  if (prop === Destroy || prop === Checkout) {
@@ -701,10 +718,19 @@ class SchemaRecord {
701
718
  return fields.has(prop);
702
719
  },
703
720
  getOwnPropertyDescriptor(target, prop) {
704
- if (!fields.has(prop)) {
705
- throw new Error(`No field named ${String(prop)} on ${identifier.type}`);
721
+ const schemaForField = prop === identityField?.name ? identityField : fields.get(prop);
722
+ macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
723
+ if (!test) {
724
+ throw new Error(`No field named ${String(prop)} on ${identifier.type}`);
725
+ }
726
+ })(schemaForField) : {};
727
+ if (isNonEnumerableProp(prop)) {
728
+ return {
729
+ writable: false,
730
+ enumerable: false,
731
+ configurable: true
732
+ };
706
733
  }
707
- const schemaForField = fields.get(prop);
708
734
  switch (schemaForField.kind) {
709
735
  case 'derived':
710
736
  return {
@@ -712,6 +738,12 @@ class SchemaRecord {
712
738
  enumerable: true,
713
739
  configurable: true
714
740
  };
741
+ case '@id':
742
+ return {
743
+ writable: identifier.id === null,
744
+ enumerable: true,
745
+ configurable: true
746
+ };
715
747
  case '@local':
716
748
  case 'field':
717
749
  case 'attribute':
@@ -729,28 +761,18 @@ class SchemaRecord {
729
761
  enumerable: true,
730
762
  configurable: true
731
763
  };
764
+ default:
765
+ return {
766
+ writable: false,
767
+ enumerable: false,
768
+ configurable: false
769
+ };
732
770
  }
733
771
  },
734
772
  get(target, prop, receiver) {
735
773
  if (RecordSymbols.has(prop)) {
736
774
  return target[prop];
737
775
  }
738
- if (prop === Symbol.toStringTag) {
739
- return `SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})>`;
740
- }
741
- if (prop === 'toString') {
742
- return function () {
743
- return `SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})>`;
744
- };
745
- }
746
- if (prop === 'toHTML') {
747
- return function () {
748
- return `<div>SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})></div>`;
749
- };
750
- }
751
- if (prop === Symbol.toPrimitive) {
752
- return null;
753
- }
754
776
 
755
777
  // TODO make this a symbol
756
778
  if (prop === '___notifications') {
@@ -766,18 +788,75 @@ class SchemaRecord {
766
788
  if (IgnoredGlobalFields.has(prop)) {
767
789
  return undefined;
768
790
  }
791
+
792
+ /////////////////////////////////////////////////////////////
793
+ //// Note these bound function behaviors are essentially ////
794
+ //// built-in but overrideable derivations. ////
795
+ //// ////
796
+ //// The bar for this has to be "basic expectations of ////
797
+ /// an object" – very, very high ////
798
+ /////////////////////////////////////////////////////////////
799
+
800
+ if (prop === Symbol.toStringTag || prop === 'toString') {
801
+ let fn = BoundFns.get('toString');
802
+ if (!fn) {
803
+ fn = function () {
804
+ entangleSignal(signals, receiver, '@identity');
805
+ return `Record<${identifier.type}:${identifier.id} (${identifier.lid})>`;
806
+ };
807
+ BoundFns.set(prop, fn);
808
+ }
809
+ return fn;
810
+ }
811
+ if (prop === 'toHTML') {
812
+ let fn = BoundFns.get('toHTML');
813
+ if (!fn) {
814
+ fn = function () {
815
+ entangleSignal(signals, receiver, '@identity');
816
+ return `<span>Record<${identifier.type}:${identifier.id} (${identifier.lid})></span>`;
817
+ };
818
+ BoundFns.set(prop, fn);
819
+ }
820
+ return fn;
821
+ }
822
+ if (prop === 'toJSON') {
823
+ let fn = BoundFns.get('toJSON');
824
+ if (!fn) {
825
+ fn = function () {
826
+ const json = {};
827
+ for (const key in receiver) {
828
+ json[key] = receiver[key];
829
+ }
830
+ return json;
831
+ };
832
+ BoundFns.set(prop, fn);
833
+ }
834
+ return fn;
835
+ }
836
+ if (prop === Symbol.toPrimitive) return () => null;
837
+ if (prop === Symbol.iterator) {
838
+ let fn = BoundFns.get(Symbol.iterator);
839
+ if (!fn) {
840
+ fn = function* () {
841
+ for (const key in receiver) {
842
+ yield [key, receiver[key]];
843
+ }
844
+ };
845
+ BoundFns.set(Symbol.iterator, fn);
846
+ }
847
+ return fn;
848
+ }
769
849
  if (prop === 'constructor') {
770
850
  return SchemaRecord;
771
851
  }
772
852
  // too many things check for random symbols
773
- if (typeof prop === 'symbol') {
774
- return undefined;
775
- }
776
- let type = identifier.type;
777
- if (isEmbedded) {
778
- type = embeddedType;
779
- }
780
- throw new Error(`No field named ${String(prop)} on ${type}`);
853
+ if (typeof prop === 'symbol') return undefined;
854
+ macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
855
+ {
856
+ throw new Error(`No field named ${String(prop)} on ${isEmbedded ? embeddedType : identifier.type}`);
857
+ }
858
+ })() : {};
859
+ return undefined;
781
860
  }
782
861
  const field = maybeField.kind === 'alias' ? maybeField.options : maybeField;
783
862
  macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
@@ -805,49 +884,24 @@ class SchemaRecord {
805
884
  return lastValue;
806
885
  }
807
886
  case 'field':
808
- macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
809
- if (!test) {
810
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
811
- }
812
- })(!target[Legacy]) : {};
813
887
  entangleSignal(signals, receiver, field.name);
814
888
  return computeField(schema, cache, target, identifier, field, propArray, IS_EDITABLE);
815
889
  case 'attribute':
816
890
  entangleSignal(signals, receiver, field.name);
817
891
  return computeAttribute(cache, identifier, prop, IS_EDITABLE);
818
892
  case 'resource':
819
- macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
820
- if (!test) {
821
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
822
- }
823
- })(!target[Legacy]) : {};
824
893
  entangleSignal(signals, receiver, field.name);
825
894
  return computeResource(store, cache, target, identifier, field, prop, IS_EDITABLE);
826
895
  case 'derived':
827
896
  return computeDerivation(schema, receiver, identifier, field, prop);
828
897
  case 'schema-array':
829
898
  case 'array':
830
- macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
831
- if (!test) {
832
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
833
- }
834
- })(!target[Legacy]) : {};
835
899
  entangleSignal(signals, receiver, field.name);
836
900
  return computeArray(store, schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
837
901
  case 'object':
838
- macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
839
- if (!test) {
840
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
841
- }
842
- })(!target[Legacy]) : {};
843
902
  entangleSignal(signals, receiver, field.name);
844
903
  return computeObject(schema, cache, target, identifier, field, propArray, Mode[Editable], Mode[Legacy]);
845
904
  case 'schema-object':
846
- macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
847
- if (!test) {
848
- throw new Error(`SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`);
849
- }
850
- })(!target[Legacy]) : {};
851
905
  entangleSignal(signals, receiver, field.name);
852
906
  // run transform, then use that value as the object to manage
853
907
  return computeSchemaObject(store, cache, target, identifier, field, propArray, Mode[Legacy], Mode[Editable]);
@@ -1370,19 +1424,28 @@ function teardownRecord(record) {
1370
1424
  assertSchemaRecord(record);
1371
1425
  record[Destroy]();
1372
1426
  }
1427
+
1428
+ /**
1429
+ * @module @warp-drive-mirror/schema-record
1430
+ */
1373
1431
  const Support = getOrSetGlobal('Support', new WeakMap());
1374
- const SchemaRecordFields = [{
1432
+ const ConstructorField = {
1375
1433
  type: '@constructor',
1376
1434
  name: 'constructor',
1377
1435
  kind: 'derived'
1378
- }, {
1436
+ };
1437
+ const TypeField = {
1379
1438
  type: '@identity',
1380
1439
  name: '$type',
1381
1440
  kind: 'derived',
1382
1441
  options: {
1383
1442
  key: 'type'
1384
1443
  }
1385
- }];
1444
+ };
1445
+ const DefaultIdentityField = {
1446
+ name: 'id',
1447
+ kind: '@id'
1448
+ };
1386
1449
  function _constructor(record) {
1387
1450
  let state = Support.get(record);
1388
1451
  if (!state) {
@@ -1392,19 +1455,64 @@ function _constructor(record) {
1392
1455
  return state._constructor = state._constructor || {
1393
1456
  name: `SchemaRecord<${recordIdentifierFor$1(record).type}>`,
1394
1457
  get modelName() {
1395
- throw new Error('Cannot access record.constructor.modelName on non-Legacy Schema Records.');
1458
+ macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
1459
+ {
1460
+ throw new Error(`record.constructor.modelName is not available outside of legacy mode`);
1461
+ }
1462
+ })() : {};
1463
+ return undefined;
1396
1464
  }
1397
1465
  };
1398
1466
  }
1399
1467
  _constructor[Type] = '@constructor';
1468
+
1469
+ /**
1470
+ * Utility for constructing a ResourceSchema with the recommended fields
1471
+ * for the Polaris experience.
1472
+ *
1473
+ * @method withDefaults
1474
+ * @for @warp-drive-mirror/schema-record
1475
+ * @static
1476
+ * @public
1477
+ * @param schema
1478
+ * @return {ResourceSchema}
1479
+ */
1400
1480
  function withDefaults(schema) {
1401
- schema.identity = schema.identity || {
1402
- name: 'id',
1403
- kind: '@id'
1404
- };
1405
- schema.fields.push(...SchemaRecordFields);
1481
+ schema.identity = schema.identity || DefaultIdentityField;
1482
+
1483
+ // because fields gets iterated in definition order,
1484
+ // we add TypeField to the beginning so that it will
1485
+ // appear right next to the identity field
1486
+ schema.fields.unshift(TypeField);
1487
+ schema.fields.push(ConstructorField);
1406
1488
  return schema;
1407
1489
  }
1490
+
1491
+ /**
1492
+ * A derivation that computes its value from the
1493
+ * record's identity.
1494
+ *
1495
+ * It can be used via a derived field definition like:
1496
+ *
1497
+ * ```ts
1498
+ * {
1499
+ * kind: 'derived',
1500
+ * name: 'id',
1501
+ * type: '@identity',
1502
+ * options: { key: 'id' }
1503
+ * }
1504
+ * ```
1505
+ *
1506
+ * Valid keys are `'id'`, `'lid'`, `'type'`, and `'^'`.
1507
+ *
1508
+ * `^` returns the entire identifier object.
1509
+ *
1510
+ * @method fromIdentity
1511
+ * @for @warp-drive-mirror/schema-record
1512
+ * @static
1513
+ * @public
1514
+ */
1515
+
1408
1516
  function fromIdentity(record, options, key) {
1409
1517
  const identifier = record[Identifier];
1410
1518
  macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
@@ -1420,6 +1528,16 @@ function fromIdentity(record, options, key) {
1420
1528
  return options.key === '^' ? identifier : identifier[options.key];
1421
1529
  }
1422
1530
  fromIdentity[Type] = '@identity';
1531
+
1532
+ /**
1533
+ * Registers the default derivations for the SchemaRecord
1534
+ *
1535
+ * @method registerDerivations
1536
+ * @for @warp-drive-mirror/schema-record
1537
+ * @static
1538
+ * @public
1539
+ * @param {SchemaService} schema
1540
+ */
1423
1541
  function registerDerivations(schema) {
1424
1542
  schema.registerDerivation(fromIdentity);
1425
1543
  schema.registerDerivation(_constructor);
@@ -1428,9 +1546,7 @@ function registerDerivations(schema) {
1428
1546
  * Wraps a derivation in a new function with Derivation signature but that looks
1429
1547
  * up the value in the cache before recomputing.
1430
1548
  *
1431
- * @param record
1432
- * @param options
1433
- * @param prop
1549
+ * @internal
1434
1550
  */
1435
1551
  function makeCachedDerivation(derivation) {
1436
1552
  const memoizedDerivation = (record, options, prop) => {
@@ -1447,6 +1563,12 @@ function makeCachedDerivation(derivation) {
1447
1563
  memoizedDerivation[Type] = derivation[Type];
1448
1564
  return memoizedDerivation;
1449
1565
  }
1566
+ /**
1567
+ * A SchemaService designed to work with dynamically registered schemas.
1568
+ *
1569
+ * @class SchemaService
1570
+ * @public
1571
+ */
1450
1572
  class SchemaService {
1451
1573
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1452
1574
 
@@ -1456,6 +1578,9 @@ class SchemaService {
1456
1578
  this._hashFns = new Map();
1457
1579
  this._derivations = new Map();
1458
1580
  }
1581
+ resourceTypes() {
1582
+ return Array.from(this._schemas.keys());
1583
+ }
1459
1584
  hasTrait(type) {
1460
1585
  return this._traits.has(type);
1461
1586
  }