@nymphjs/nymph 1.0.0-beta.57 → 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 +10 -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 +37 -11
  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 +55 -15
  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 {
@@ -212,10 +212,10 @@ export default class Nymph {
212
212
  }
213
213
 
214
214
  this.driver.init(this);
215
+ this.config.debugLog('nymph', 'Nymph driver loaded.');
215
216
  if (this.tilmeld) {
216
- this.config.debugLog('nymph', 'Tilmeld initializing.');
217
217
  this.tilmeld.init(this);
218
- this.config.debugLog('nymph', 'Tilmeld initialized.');
218
+ this.config.debugLog('nymph', 'Tilmeld loaded.');
219
219
  }
220
220
  }
221
221
 
@@ -229,25 +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
- this.config.debugLog('nymph', `Added entity class "${entityClass.class}".`);
243
- return NymphEntity;
260
+ };
261
+ return this.entityClasses[EntityClass.class] as T;
244
262
  }
245
263
 
246
264
  /**
247
265
  * Get the class that uses the specified class name.
248
266
  */
249
- public getEntityClass<T extends EntityConstructor>(className: T): T;
250
267
  public getEntityClass(className: string): EntityConstructor;
268
+ public getEntityClass<T extends EntityConstructor>(className: T): T;
251
269
  public getEntityClass<T extends EntityConstructor = EntityConstructor>(
252
270
  className: T | string,
253
271
  ): T | EntityConstructor {
@@ -264,6 +282,28 @@ export default class Nymph {
264
282
  throw new ClassNotAvailableError(`Tried to use missing class "${key}".`);
265
283
  }
266
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
+
267
307
  /**
268
308
  * Get a clone of this instance with cloned classes and event listeners.
269
309
  *
@@ -820,11 +860,11 @@ export default class Nymph {
820
860
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
821
861
  options?: Options<T>,
822
862
  ...selectors: Selector[]
823
- ): Promise<ReturnType<T['factorySync']>[]>;
863
+ ): Promise<EntityInstanceType<T>[]>;
824
864
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
825
865
  options: Options<T> = {},
826
866
  ...selectors: Selector[]
827
- ): Promise<ReturnType<T['factorySync']>[] | string[] | number> {
867
+ ): Promise<EntityInstanceType<T>[] | string[] | number> {
828
868
  try {
829
869
  for (let callback of this.beforeGetEntitiesCallbacks) {
830
870
  if (callback) {
@@ -862,7 +902,7 @@ export default class Nymph {
862
902
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
863
903
  options: Options<T>,
864
904
  ...selectors: Selector[]
865
- ): Promise<ReturnType<T['factorySync']> | null>;
905
+ ): Promise<EntityInstanceType<T> | null>;
866
906
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
867
907
  options: Options<T> & { return: 'guid' },
868
908
  guid: string,
@@ -870,11 +910,11 @@ export default class Nymph {
870
910
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
871
911
  options: Options<T>,
872
912
  guid: string,
873
- ): Promise<ReturnType<T['factorySync']> | null>;
913
+ ): Promise<EntityInstanceType<T> | null>;
874
914
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
875
915
  options: Options<T> = {},
876
916
  ...selectors: Selector[] | string[]
877
- ): Promise<ReturnType<T['factorySync']> | string | number | null> {
917
+ ): Promise<EntityInstanceType<T> | string | number | null> {
878
918
  try {
879
919
  // Set up options and selectors.
880
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'