@nymphjs/nymph 1.0.0-beta.58 → 1.0.0-beta.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +0 -8
  3. package/dist/Entity.d.ts +65 -6
  4. package/dist/Entity.js +53 -1
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +1 -0
  7. package/dist/Entity.test.js.map +1 -1
  8. package/dist/Entity.types.d.ts +15 -70
  9. package/dist/Nymph.d.ts +14 -6
  10. package/dist/Nymph.js +35 -8
  11. package/dist/Nymph.js.map +1 -1
  12. package/dist/Nymph.types.d.ts +2 -2
  13. package/dist/driver/NymphDriver.d.ts +47 -11
  14. package/dist/driver/NymphDriver.js +28 -5
  15. package/dist/driver/NymphDriver.js.map +1 -1
  16. package/dist/driver/testArtifacts.d.ts +1 -1
  17. package/dist/driver/testArtifacts.js +77 -3
  18. package/dist/driver/testArtifacts.js.map +1 -1
  19. package/dist/errors/EntityUniqueConstraintError.d.ts +3 -0
  20. package/dist/errors/EntityUniqueConstraintError.js +11 -0
  21. package/dist/errors/EntityUniqueConstraintError.js.map +1 -0
  22. package/dist/errors/index.d.ts +1 -0
  23. package/dist/errors/index.js +1 -0
  24. package/dist/errors/index.js.map +1 -1
  25. package/dist/testArtifacts.d.ts +2 -6
  26. package/dist/testArtifacts.js +4 -18
  27. package/dist/testArtifacts.js.map +1 -1
  28. package/dist/testMocks.d.ts +9 -7
  29. package/dist/testMocks.js +19 -10
  30. package/dist/testMocks.js.map +1 -1
  31. package/package.json +3 -3
  32. package/src/Entity.test.ts +1 -0
  33. package/src/Entity.ts +113 -15
  34. package/src/Entity.types.ts +18 -70
  35. package/src/Nymph.ts +53 -12
  36. package/src/Nymph.types.ts +2 -2
  37. package/src/driver/NymphDriver.ts +74 -67
  38. package/src/driver/testArtifacts.ts +102 -1
  39. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  40. package/src/errors/index.ts +2 -0
  41. package/src/testArtifacts.ts +8 -27
  42. package/src/testMocks.ts +32 -17
package/src/Entity.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { difference, intersection, isEqual } from 'lodash';
2
2
 
3
3
  import type Nymph from './Nymph';
4
- import {
4
+ import type { Options } from './Nymph.types';
5
+ import type {
5
6
  ACProperties,
6
7
  EntityConstructor,
7
8
  EntityData,
@@ -23,6 +24,13 @@ import {
23
24
  uniqueStrings,
24
25
  } from './utils';
25
26
 
27
+ export type EntityDataType<T> = T extends Entity<infer DataType>
28
+ ? DataType
29
+ : never;
30
+
31
+ export type EntityInstanceType<T extends EntityConstructor> =
32
+ T extends new () => infer E ? E & EntityDataType<E> : never;
33
+
26
34
  /**
27
35
  * Database abstraction object.
28
36
  *
@@ -230,10 +238,18 @@ export default class Entity<T extends EntityData = EntityData>
230
238
  */
231
239
  private $originalAcValues: ACProperties | null = null;
232
240
 
241
+ /**
242
+ * Alter the options for a query for this entity.
243
+ *
244
+ * @param options The current options.
245
+ * @returns The altered options.
246
+ */
247
+ static alterOptions?<T extends Options>(options: T): T;
248
+
233
249
  /**
234
250
  * Initialize an entity.
235
251
  */
236
- public constructor() {
252
+ public constructor(..._rest: any[]) {
237
253
  this.$nymph = (this.constructor as EntityConstructor).nymph;
238
254
  this.$dataHandler = {
239
255
  has: (data: EntityData, name: string) => {
@@ -432,30 +448,108 @@ export default class Entity<T extends EntityData = EntityData>
432
448
  }) as Entity<T>;
433
449
  }
434
450
 
435
- public static async factory(guid?: string) {
451
+ /**
452
+ * Create or retrieve a new entity instance.
453
+ *
454
+ * Note that this will always return an entity, even if the GUID is not found.
455
+ *
456
+ * @param guid An optional GUID to retrieve.
457
+ */
458
+ public static async factory<E extends Entity>(
459
+ this: {
460
+ new (): E;
461
+ },
462
+ guid?: string,
463
+ ): Promise<E & EntityDataType<E>> {
436
464
  const entity = new this();
437
465
  if (guid != null) {
438
- const entity = await this.nymph.getEntity(
466
+ const entity = await (
467
+ this as unknown as EntityConstructor
468
+ ).nymph.getEntity(
439
469
  {
440
- class: this as any as EntityConstructor,
470
+ class: this as unknown as EntityConstructor,
441
471
  },
442
472
  { type: '&', guid },
443
473
  );
444
474
  if (entity != null) {
445
- return entity;
475
+ return entity as E & EntityDataType<E>;
446
476
  }
447
477
  }
448
- return entity;
478
+ return entity as E & EntityDataType<E>;
449
479
  }
450
480
 
451
- public static factorySync() {
452
- return new this();
481
+ /**
482
+ * Create a new entity instance.
483
+ */
484
+ public static factorySync<E extends Entity>(this: {
485
+ new (): E;
486
+ }): E & EntityDataType<E> {
487
+ return new this() as E & EntityDataType<E>;
453
488
  }
454
489
 
455
- public static factoryReference(reference: EntityReference) {
490
+ /**
491
+ * Create a new sleeping reference instance.
492
+ *
493
+ * Sleeping references won't retrieve their data from the database until they
494
+ * are readied with `$wake()` or a parent's `$wakeAll()`.
495
+ *
496
+ * @param reference The Nymph Entity Reference to use to wake.
497
+ * @returns The new instance.
498
+ */
499
+ public static factoryReference<E extends Entity>(
500
+ this: {
501
+ new (): E;
502
+ },
503
+ reference: EntityReference,
504
+ ): E & EntityDataType<E> {
456
505
  const entity = new this();
457
506
  entity.$referenceSleep(reference);
458
- return entity;
507
+ return entity as E & EntityDataType<E>;
508
+ }
509
+
510
+ /**
511
+ * Get an array of strings that **must** be unique across the current etype.
512
+ *
513
+ * When you try to save another entity with any of the same unique strings,
514
+ * Nymph will throw an error.
515
+ *
516
+ * The default implementation of this static method instantiates the entity,
517
+ * assigns all of the given data, then calls `$getUniques` and returns its
518
+ * output. This can have a performance impact if a lot of extra processing
519
+ * happens during any of these steps. You can override this method to
520
+ * calculate the unique strings faster, but you must return the same strings
521
+ * that would be returned by `$getUniques`.
522
+ *
523
+ * @returns Resolves to an array of entity's unique constraint strings.
524
+ */
525
+ public static async getUniques({
526
+ guid,
527
+ cdate,
528
+ mdate,
529
+ tags,
530
+ data,
531
+ sdata,
532
+ }: {
533
+ guid?: string;
534
+ cdate?: number;
535
+ mdate?: number;
536
+ tags: string[];
537
+ data: EntityData;
538
+ sdata?: SerializedEntityData;
539
+ }) {
540
+ const entity = new this();
541
+ if (guid != null) {
542
+ entity.guid = guid;
543
+ }
544
+ if (cdate != null) {
545
+ entity.cdate = cdate;
546
+ }
547
+ if (mdate != null) {
548
+ entity.mdate = mdate;
549
+ }
550
+ entity.tags = tags;
551
+ entity.$putData(data, sdata);
552
+ return await entity.$getUniques();
459
553
  }
460
554
 
461
555
  public toJSON() {
@@ -564,6 +658,10 @@ export default class Entity<T extends EntityData = EntityData>
564
658
  return this.$sdata;
565
659
  }
566
660
 
661
+ public async $getUniques(): Promise<string[]> {
662
+ return [];
663
+ }
664
+
567
665
  public $getOriginalAcValues(): ACProperties {
568
666
  this.$check();
569
667
 
@@ -719,8 +817,8 @@ export default class Entity<T extends EntityData = EntityData>
719
817
  protectedProps.push('acWrite');
720
818
  protectedProps.push('acFull');
721
819
  if (
722
- ((this.constructor as typeof Entity).class !== 'User' &&
723
- (this.constructor as typeof Entity).class !== 'Group') ||
820
+ ((this.constructor as EntityConstructor).class !== 'User' &&
821
+ (this.constructor as EntityConstructor).class !== 'Group') ||
724
822
  !(this.$nymph.tilmeld as any).currentUser ||
725
823
  (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
726
824
  'tilmeld/admin',
@@ -789,8 +887,8 @@ export default class Entity<T extends EntityData = EntityData>
789
887
  protectedProps.push('acWrite');
790
888
  protectedProps.push('acFull');
791
889
  if (
792
- ((this.constructor as typeof Entity).class !== 'User' &&
793
- (this.constructor as typeof Entity).class !== 'Group') ||
890
+ ((this.constructor as EntityConstructor).class !== 'User' &&
891
+ (this.constructor as EntityConstructor).class !== 'Group') ||
794
892
  !(this.$nymph.tilmeld as any).currentUser ||
795
893
  (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
796
894
  'tilmeld/admin',
@@ -1,5 +1,5 @@
1
1
  import type Nymph from './Nymph';
2
- import type { Options } from './Nymph.types';
2
+ import type Entity from './Entity';
3
3
 
4
4
  export type ACProperties = {
5
5
  user: string | null;
@@ -178,6 +178,18 @@ export interface EntityInterface extends DataObjectInterface {
178
178
  * @returns The entity's serialized data object.
179
179
  */
180
180
  $getSData(): SerializedEntityData;
181
+ /**
182
+ * Get an array of strings that **must** be unique across the current etype.
183
+ *
184
+ * When you try to save another entity with any of the same unique strings,
185
+ * Nymph will throw an error.
186
+ *
187
+ * The default implementation of this method returns an empty array, meaning
188
+ * there are no uniqueness constraints applied to its etype.
189
+ *
190
+ * @returns Resolves to an array of entity's unique constraint strings.
191
+ */
192
+ $getUniques(): Promise<string[]>;
181
193
  /**
182
194
  * Get the original values of the AC properties.
183
195
  *
@@ -277,73 +289,9 @@ export interface EntityInterface extends DataObjectInterface {
277
289
  $useSkipAc(useSkipAc: boolean): void;
278
290
  }
279
291
 
280
- export type EntityConstructor = (new (...args: any[]) => EntityInterface) & {
281
- /**
282
- * The instance of Nymph to use for queries.
283
- */
284
- nymph: Nymph;
285
- /**
286
- * A unique name for this type of entity used to separate its data from other
287
- * types of entities in the database.
288
- */
289
- ETYPE: string;
290
- /**
291
- * The lookup name for this entity.
292
- *
293
- * This is used for reference arrays (and sleeping references) and client
294
- * requests.
295
- */
296
- class: string;
297
- /**
298
- * Whether this entity should publish changes to PubSub servers.
299
- */
300
- pubSubEnabled: boolean;
301
- /**
302
- * Whether this entity should be accessible on the frontend through the REST
303
- * server.
304
- *
305
- * If this is false, any request from the client that attempts to use this
306
- * entity will fail.
307
- */
308
- restEnabled: boolean;
309
- /**
310
- * Properties that will not be searchable from the frontend. If the frontend
311
- * includes any of these properties in any of their clauses, they will be
312
- * filtered out before the search is executed.
313
- */
314
- searchRestrictedData: string[];
315
- /**
316
- * The names of static methods allowed to be called by the frontend with
317
- * serverCallStatic.
318
- */
319
- clientEnabledStaticMethods: string[];
320
- /**
321
- * Create a new entity instance.
322
- *
323
- * @param guid An optional GUID to retrieve.
324
- */
325
- factory(guid?: string): Promise<EntityInterface>;
326
- /**
327
- * Create a new entity instance.
328
- *
329
- * @param guid An optional GUID to retrieve.
330
- */
331
- factorySync(guid?: string): EntityInterface;
332
- /**
333
- * Create a new sleeping reference instance.
334
- *
335
- * Sleeping references won't retrieve their data from the database until it
336
- * is actually used.
337
- *
338
- * @param reference The Nymph Entity Reference to use to wake.
339
- * @returns The new instance.
340
- */
341
- factoryReference(reference: EntityReference): EntityInterface;
342
- /**
343
- * Alter the options for a query for this entity.
344
- *
345
- * @param options The current options.
346
- * @returns The altered options.
347
- */
348
- alterOptions?<T extends Options>(options: T): T;
292
+ export type EntityConstructor<
293
+ D extends EntityData = EntityData,
294
+ E extends Entity<D> = Entity<D>,
295
+ > = (new (...args: any[]) => E) & {
296
+ [k in keyof typeof Entity]: (typeof Entity)[k];
349
297
  };
package/src/Nymph.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Config, ConfigDefaults as defaults } from './conf';
2
2
  import type { NymphDriver } from './driver';
3
- import Entity from './Entity';
3
+ import Entity, { type EntityInstanceType } from './Entity';
4
4
  import type { EntityConstructor, EntityInterface } from './Entity.types';
5
5
  import { ClassNotAvailableError } from './errors';
6
6
  import type {
@@ -229,24 +229,43 @@ export default class Nymph {
229
229
  * Because this creates a subclass, don't use the class
230
230
  * returned from `getEntityClass` to check with `instanceof`.
231
231
  */
232
- public addEntityClass<T extends EntityConstructor>(entityClass: T): T {
232
+ public addEntityClass<T extends EntityConstructor>(EntityClass: T): T {
233
+ if (EntityClass.class in this.entityClasses) {
234
+ this.config.debugLog(
235
+ 'nymph',
236
+ `Adding duplicate class "${EntityClass.class}". This may be a mistake.`,
237
+ );
238
+ }
239
+
240
+ // Check that it doesn't have the same etype as any other class.
241
+ for (const ExistingClass of Object.values(this.entityClasses)) {
242
+ if (
243
+ EntityClass.ETYPE === ExistingClass.ETYPE &&
244
+ EntityClass.class !== EntityClass.class
245
+ ) {
246
+ this.config.debugLog(
247
+ 'nymph',
248
+ `Adding class "${EntityClass.class}" with same etype as existing class "${ExistingClass.class}". This may be a mistake.`,
249
+ );
250
+ }
251
+ }
252
+
233
253
  const nymph = this;
234
- class NymphEntity extends entityClass {
254
+ this.entityClasses[EntityClass.class] = class extends EntityClass {
235
255
  static nymph: Nymph = nymph;
236
256
 
237
257
  constructor(...args: any[]) {
238
258
  super(...args);
239
259
  }
240
- }
241
- this.entityClasses[entityClass.class] = NymphEntity;
242
- return NymphEntity;
260
+ };
261
+ return this.entityClasses[EntityClass.class] as T;
243
262
  }
244
263
 
245
264
  /**
246
265
  * Get the class that uses the specified class name.
247
266
  */
248
- public getEntityClass<T extends EntityConstructor>(className: T): T;
249
267
  public getEntityClass(className: string): EntityConstructor;
268
+ public getEntityClass<T extends EntityConstructor>(className: T): T;
250
269
  public getEntityClass<T extends EntityConstructor = EntityConstructor>(
251
270
  className: T | string,
252
271
  ): T | EntityConstructor {
@@ -263,6 +282,28 @@ export default class Nymph {
263
282
  throw new ClassNotAvailableError(`Tried to use missing class "${key}".`);
264
283
  }
265
284
 
285
+ /**
286
+ * Get the class that uses the specified etype.
287
+ *
288
+ * Note that it is fine, though unusual, for two classes to use the same
289
+ * etype. However, this can lead to very hard to diagnose bugs, so is
290
+ * generally discouraged.
291
+ */
292
+ public getEntityClassByEtype(etype: string): EntityConstructor {
293
+ for (const EntityClass of Object.values(this.entityClasses)) {
294
+ if (EntityClass.ETYPE === etype) {
295
+ return EntityClass;
296
+ }
297
+ }
298
+ this.config.debugError(
299
+ 'nymph',
300
+ `Tried to use missing class by etype "${etype}".`,
301
+ );
302
+ throw new ClassNotAvailableError(
303
+ `Tried to use missing class by etype "${etype}".`,
304
+ );
305
+ }
306
+
266
307
  /**
267
308
  * Get a clone of this instance with cloned classes and event listeners.
268
309
  *
@@ -819,11 +860,11 @@ export default class Nymph {
819
860
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
820
861
  options?: Options<T>,
821
862
  ...selectors: Selector[]
822
- ): Promise<ReturnType<T['factorySync']>[]>;
863
+ ): Promise<EntityInstanceType<T>[]>;
823
864
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
824
865
  options: Options<T> = {},
825
866
  ...selectors: Selector[]
826
- ): Promise<ReturnType<T['factorySync']>[] | string[] | number> {
867
+ ): Promise<EntityInstanceType<T>[] | string[] | number> {
827
868
  try {
828
869
  for (let callback of this.beforeGetEntitiesCallbacks) {
829
870
  if (callback) {
@@ -861,7 +902,7 @@ export default class Nymph {
861
902
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
862
903
  options: Options<T>,
863
904
  ...selectors: Selector[]
864
- ): Promise<ReturnType<T['factorySync']> | null>;
905
+ ): Promise<EntityInstanceType<T> | null>;
865
906
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
866
907
  options: Options<T> & { return: 'guid' },
867
908
  guid: string,
@@ -869,11 +910,11 @@ export default class Nymph {
869
910
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
870
911
  options: Options<T>,
871
912
  guid: string,
872
- ): Promise<ReturnType<T['factorySync']> | null>;
913
+ ): Promise<EntityInstanceType<T> | null>;
873
914
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
874
915
  options: Options<T> = {},
875
916
  ...selectors: Selector[] | string[]
876
- ): Promise<ReturnType<T['factorySync']> | string | number | null> {
917
+ ): Promise<EntityInstanceType<T> | string | number | null> {
877
918
  try {
878
919
  // Set up options and selectors.
879
920
  if (typeof selectors[0] === 'string') {
@@ -1,5 +1,5 @@
1
- import { EntityConstructor, EntityInterface } from './Entity.types';
2
- import Nymph from './Nymph';
1
+ import type { EntityConstructor, EntityInterface } from './Entity.types';
2
+ import type Nymph from './Nymph';
3
3
 
4
4
  export type NymphEventType =
5
5
  | 'connect'