@nymphjs/nymph 1.0.0-beta.91 → 1.0.0-beta.93

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.
@@ -22,6 +22,14 @@ export type SerializedEntityData = {
22
22
  [k: string]: string;
23
23
  };
24
24
 
25
+ export type EntityObject = {
26
+ guid: string | null;
27
+ cdate: number | null;
28
+ mdate: number | null;
29
+ tags: string[];
30
+ [k: string]: any;
31
+ };
32
+
25
33
  export type EntityJson = {
26
34
  class: string;
27
35
  guid: string | null;
package/src/Nymph.ts CHANGED
@@ -1,8 +1,14 @@
1
1
  import { Config, ConfigDefaults as defaults } from './conf/index.js';
2
2
  import type { NymphDriver } from './driver/index.js';
3
- import Entity, { type EntityInstanceType } from './Entity.js';
3
+ import Entity, {
4
+ type EntityInstanceType,
5
+ type EntityObjectType,
6
+ } from './Entity.js';
4
7
  import type { EntityConstructor, EntityInterface } from './Entity.types.js';
5
- import { ClassNotAvailableError } from './errors/index.js';
8
+ import {
9
+ ClassNotAvailableError,
10
+ InvalidParametersError,
11
+ } from './errors/index.js';
6
12
  import type {
7
13
  Selector,
8
14
  Options,
@@ -702,8 +708,8 @@ export default class Nymph {
702
708
  * a top-level property. The method of sorting properties other than cdate
703
709
  * and mdate is driver dependent. The only hard rule is that numbers should
704
710
  * be sorted numerically (2 before 10). Defaults to "cdate".
705
- * - return - What to return. "entity", "guid", or "count". Defaults to
706
- * "entity".
711
+ * - return - What to return. "entity", "object", "guid", or "count". Defaults
712
+ * to "entity".
707
713
  * - source - Will be 'client' if the query came from a REST call.
708
714
  * - skipCache - If true, Nymph will skip the cache and retrieve the entity
709
715
  * from the DB.
@@ -857,6 +863,10 @@ export default class Nymph {
857
863
  options: Options<T> & { return: 'guid' },
858
864
  ...selectors: Selector[]
859
865
  ): Promise<string[]>;
866
+ public async getEntities<T extends EntityConstructor = EntityConstructor>(
867
+ options: Options<T> & { return: 'object' },
868
+ ...selectors: Selector[]
869
+ ): Promise<EntityObjectType<T>[]>;
860
870
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
861
871
  options?: Options<T>,
862
872
  ...selectors: Selector[]
@@ -864,7 +874,15 @@ export default class Nymph {
864
874
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
865
875
  options: Options<T> = {},
866
876
  ...selectors: Selector[]
867
- ): Promise<EntityInstanceType<T>[] | string[] | number> {
877
+ ): Promise<
878
+ EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
879
+ > {
880
+ if (options.source === 'client' && options.return === 'object') {
881
+ throw new InvalidParametersError(
882
+ 'Object return type not allowed from client.',
883
+ );
884
+ }
885
+
868
886
  try {
869
887
  for (let callback of this.beforeGetEntitiesCallbacks) {
870
888
  if (callback) {
@@ -899,6 +917,10 @@ export default class Nymph {
899
917
  options: Options<T> & { return: 'guid' },
900
918
  ...selectors: Selector[]
901
919
  ): Promise<string | null>;
920
+ public async getEntity<T extends EntityConstructor = EntityConstructor>(
921
+ options: Options<T> & { return: 'object' },
922
+ ...selectors: Selector[]
923
+ ): Promise<EntityObjectType<T> | null>;
902
924
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
903
925
  options: Options<T>,
904
926
  ...selectors: Selector[]
@@ -907,6 +929,10 @@ export default class Nymph {
907
929
  options: Options<T> & { return: 'guid' },
908
930
  guid: string,
909
931
  ): Promise<string | null>;
932
+ public async getEntity<T extends EntityConstructor = EntityConstructor>(
933
+ options: Options<T> & { return: 'object' },
934
+ guid: string,
935
+ ): Promise<EntityObjectType<T> | null>;
910
936
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
911
937
  options: Options<T>,
912
938
  guid: string,
@@ -914,7 +940,15 @@ export default class Nymph {
914
940
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
915
941
  options: Options<T> = {},
916
942
  ...selectors: Selector[] | string[]
917
- ): Promise<EntityInstanceType<T> | string | number | null> {
943
+ ): Promise<
944
+ EntityInstanceType<T> | EntityObjectType<T> | string | number | null
945
+ > {
946
+ if (options.source === 'client' && options.return === 'object') {
947
+ throw new InvalidParametersError(
948
+ 'Object return type not allowed from client.',
949
+ );
950
+ }
951
+
918
952
  try {
919
953
  // Set up options and selectors.
920
954
  if (typeof selectors[0] === 'string') {
@@ -208,10 +208,15 @@ export type Options<T extends EntityConstructor = EntityConstructor> = {
208
208
  */
209
209
  sort?: 'cdate' | 'mdate' | string;
210
210
  /**
211
- * What to return, the entities with their data, just the GUIDs, or just a
212
- * count.
211
+ * What to return, the entities with their data, just the entity data, just
212
+ * the GUIDs, or just a count.
213
+ *
214
+ * 'object' is only available on the server, as there are substantial security
215
+ * implications with using it. It returns the full entity data as a simple
216
+ * object. You should only use it for operations where instantiating the
217
+ * entities has an unacceptable performance cost.
213
218
  */
214
- return?: 'entity' | 'guid' | 'count';
219
+ return?: 'entity' | 'object' | 'guid' | 'count';
215
220
  /**
216
221
  * Will be 'client' if the query came from a REST request or the PubSub
217
222
  * server. (Mainly used in Tilmeld for access control.)
@@ -5,7 +5,7 @@ import strtotime from 'locutus/php/datetime/strtotime.js';
5
5
 
6
6
  import type Nymph from '../Nymph.js';
7
7
  import type { Selector, Options, FormattedSelector } from '../Nymph.types.js';
8
- import Entity, { type EntityInstanceType } from '../Entity.js';
8
+ import type { EntityInstanceType, EntityObjectType } from '../Entity.js';
9
9
  import type {
10
10
  EntityConstructor,
11
11
  EntityData,
@@ -108,6 +108,10 @@ export default abstract class NymphDriver {
108
108
  options: Options<T> & { return: 'guid' },
109
109
  ...selectors: Selector[]
110
110
  ): Promise<string[]>;
111
+ abstract getEntities<T extends EntityConstructor = EntityConstructor>(
112
+ options: Options<T> & { return: 'object' },
113
+ ...selectors: Selector[]
114
+ ): Promise<EntityObjectType<T>[]>;
111
115
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
112
116
  options?: Options<T>,
113
117
  ...selectors: Selector[]
@@ -115,7 +119,9 @@ export default abstract class NymphDriver {
115
119
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
116
120
  options?: Options<T>,
117
121
  ...selectors: Selector[]
118
- ): Promise<EntityInstanceType<T>[] | string[] | number>;
122
+ ): Promise<
123
+ EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
124
+ >;
119
125
  abstract getUID(name: string): Promise<number | null>;
120
126
  abstract importEntity(entity: {
121
127
  guid: string;
@@ -796,6 +802,30 @@ export default abstract class NymphDriver {
796
802
  svalue: string;
797
803
  },
798
804
  ): { result: any; process: () => string[] | Error };
805
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
806
+ options: Options<T> & { return: 'object' },
807
+ selectors: Selector[],
808
+ performQueryCallback: (data: {
809
+ options: Options<T>;
810
+ selectors: FormattedSelector[];
811
+ etype: string;
812
+ }) => {
813
+ result: any;
814
+ },
815
+ rowFetchCallback: () => any,
816
+ freeResultCallback: () => void,
817
+ getCountCallback: (row: any) => number,
818
+ getGUIDCallback: (row: any) => string,
819
+ getTagsAndDatesCallback: (row: any) => {
820
+ tags: string[];
821
+ cdate: number;
822
+ mdate: number;
823
+ },
824
+ getDataNameAndSValueCallback: (row: any) => {
825
+ name: string;
826
+ svalue: string;
827
+ },
828
+ ): { result: any; process: () => EntityObjectType<T>[] | Error };
799
829
  protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
800
830
  options: Options<T>,
801
831
  selectors: Selector[],
@@ -848,7 +878,12 @@ export default abstract class NymphDriver {
848
878
  },
849
879
  ): {
850
880
  result: any;
851
- process: () => EntityInstanceType<T>[] | string[] | number | Error;
881
+ process: () =>
882
+ | EntityInstanceType<T>[]
883
+ | EntityObjectType<T>[]
884
+ | string[]
885
+ | number
886
+ | Error;
852
887
  } {
853
888
  if (!this.isConnected()) {
854
889
  throw new UnableToConnectError('not connected to DB');
@@ -866,12 +901,19 @@ export default abstract class NymphDriver {
866
901
  }
867
902
  }
868
903
 
869
- let entities: EntityInstanceType<T>[] | string[] = [];
904
+ let entities: EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] =
905
+ [];
870
906
  let count = 0;
871
907
  const EntityClass =
872
908
  options.class ?? (this.nymph.getEntityClass('Entity') as T);
873
909
  const etype = EntityClass.ETYPE;
874
910
 
911
+ if (options.source === 'client' && options.return === 'object') {
912
+ throw new InvalidParametersError(
913
+ 'Object return type not allowed from client.',
914
+ );
915
+ }
916
+
875
917
  // Check if the requested entity is cached.
876
918
  if (
877
919
  this.nymph.config.cache &&
@@ -888,33 +930,75 @@ export default abstract class NymphDriver {
888
930
  (Object.keys(selectors[0]).length === 2 ||
889
931
  (Object.keys(selectors[0]).length === 3 && 'tag' in selectors[0]))
890
932
  ) {
891
- const entity = this.pullCache<EntityInstanceType<T>>(
892
- selectors[0]['guid'],
893
- EntityClass.class,
894
- !!options.skipAc,
895
- );
933
+ const cacheEntity = this.pullCache(selectors[0]['guid']);
934
+
896
935
  if (
897
- entity != null &&
898
- entity.guid != null &&
936
+ cacheEntity != null &&
937
+ cacheEntity.guid != null &&
899
938
  (!('tag' in selectors[0]) ||
900
- entity.hasTag(
901
- ...(Array.isArray(selectors[0].tag)
939
+ !(
940
+ Array.isArray(selectors[0].tag)
902
941
  ? selectors[0].tag
903
- : [selectors[0].tag]),
904
- ))
942
+ : [selectors[0].tag]
943
+ ).find((tag) => tag == null || !cacheEntity.tags.includes(tag)))
905
944
  ) {
906
- const guid = entity.guid;
907
- return {
908
- result: Promise.resolve(null),
909
- process: () =>
910
- options.return === 'count'
911
- ? guid
912
- ? 1
913
- : 0
914
- : options.return === 'guid'
915
- ? [guid]
916
- : [entity],
917
- };
945
+ // Return the value from the cache.
946
+ const { guid, cdate, mdate, tags, data, sdata } = cacheEntity;
947
+
948
+ if (options.return === 'count') {
949
+ return {
950
+ result: Promise.resolve(null),
951
+ process: () => 1,
952
+ };
953
+ } else if (options.return === 'guid') {
954
+ return {
955
+ result: Promise.resolve(null),
956
+ process: () => [guid],
957
+ };
958
+ } else if (options.return === 'object') {
959
+ return {
960
+ result: Promise.resolve(null),
961
+ process: () => [
962
+ {
963
+ guid,
964
+ cdate,
965
+ mdate,
966
+ tags,
967
+ ...data,
968
+ ...Object.fromEntries(
969
+ Object.entries(sdata).map(([name, value]) => [
970
+ name,
971
+ JSON.parse(value),
972
+ ]),
973
+ ),
974
+ } as EntityObjectType<T>,
975
+ ],
976
+ };
977
+ } else {
978
+ const entity = this.nymph
979
+ .getEntityClass(EntityClass.class)
980
+ .factorySync() as EntityInstanceType<T>;
981
+ if (entity) {
982
+ entity.$nymph = this.nymph;
983
+ if (options.skipAc != null) {
984
+ entity.$useSkipAc(!!options.skipAc);
985
+ }
986
+ entity.guid = guid;
987
+ entity.cdate = cdate;
988
+ entity.mdate = mdate;
989
+ entity.tags = tags;
990
+ entity.$putData(data, sdata, 'server');
991
+ return {
992
+ result: Promise.resolve(null),
993
+ process: () => [entity],
994
+ };
995
+ }
996
+
997
+ return {
998
+ result: Promise.resolve(null),
999
+ process: () => [],
1000
+ };
1001
+ }
918
1002
  }
919
1003
  }
920
1004
  }
@@ -971,26 +1055,43 @@ export default abstract class NymphDriver {
971
1055
  // Make sure that $row is incremented :)
972
1056
  row = rowFetchCallback();
973
1057
  }
974
- const entity = EntityClass.factorySync() as EntityInstanceType<T>;
975
- entity.$nymph = this.nymph;
976
- if (options.skipAc != null) {
977
- entity.$useSkipAc(!!options.skipAc);
978
- }
979
- entity.guid = guid;
980
- entity.cdate = cdate;
981
- entity.mdate = mdate;
982
- entity.tags = tags;
983
- this.putDataCounter++;
984
- if (this.putDataCounter == 100) {
985
- throw new Error('Infinite loop detected in Entity loading.');
1058
+
1059
+ if (options.return === 'object') {
1060
+ (entities as EntityObjectType<T>[]).push({
1061
+ guid,
1062
+ cdate,
1063
+ mdate,
1064
+ tags,
1065
+ ...data,
1066
+ ...Object.fromEntries(
1067
+ Object.entries(sdata).map(([name, value]) => [
1068
+ name,
1069
+ JSON.parse(value),
1070
+ ]),
1071
+ ),
1072
+ } as EntityObjectType<T>);
1073
+ } else {
1074
+ const entity =
1075
+ EntityClass.factorySync() as EntityInstanceType<T>;
1076
+ entity.$nymph = this.nymph;
1077
+ if (options.skipAc != null) {
1078
+ entity.$useSkipAc(!!options.skipAc);
1079
+ }
1080
+ entity.guid = guid;
1081
+ entity.cdate = cdate;
1082
+ entity.mdate = mdate;
1083
+ entity.tags = tags;
1084
+ this.putDataCounter++;
1085
+ if (this.putDataCounter == 100) {
1086
+ throw new Error('Infinite loop detected in Entity loading.');
1087
+ }
1088
+ entity.$putData(data, sdata, 'server');
1089
+ this.putDataCounter--;
1090
+ entities.push(entity);
986
1091
  }
987
- entity.$putData(data, sdata, 'server');
988
- this.putDataCounter--;
989
1092
  if (this.nymph.config.cache) {
990
1093
  this.pushCache(guid, cdate, mdate, tags, data, sdata);
991
1094
  }
992
- // @ts-ignore: ts doesn't know the return type here.
993
- entities.push(entity);
994
1095
  }
995
1096
  }
996
1097
 
@@ -1128,36 +1229,30 @@ export default abstract class NymphDriver {
1128
1229
  * Pull an entity from the cache.
1129
1230
  *
1130
1231
  * @param guid The entity's GUID.
1131
- * @param className The entity's class.
1132
- * @param useSkipAc Whether to tell the entity to use skip_ac.
1133
- * @returns The entity or null if it's not cached.
1232
+ * @returns The entity's data or null if it's not cached.
1134
1233
  */
1135
- protected pullCache<T extends Entity>(
1136
- guid: string,
1137
- className: string,
1138
- useSkipAc = false,
1139
- ): T | null {
1234
+ protected pullCache(guid: string): {
1235
+ guid: string;
1236
+ cdate: number;
1237
+ mdate: number;
1238
+ tags: string[];
1239
+ data: EntityData;
1240
+ sdata: SerializedEntityData;
1241
+ } | null {
1140
1242
  // Increment the entity access count.
1141
1243
  if (!(guid in this.entityCount)) {
1142
1244
  this.entityCount[guid] = 0;
1143
1245
  }
1144
1246
  this.entityCount[guid]++;
1145
1247
  if (guid in this.entityCache) {
1146
- const entity = this.nymph.getEntityClass(className).factorySync() as T;
1147
- if (entity) {
1148
- entity.$nymph = this.nymph;
1149
- entity.$useSkipAc(!!useSkipAc);
1150
- entity.guid = guid;
1151
- entity.cdate = this.entityCache[guid]['cdate'];
1152
- entity.mdate = this.entityCache[guid]['mdate'];
1153
- entity.tags = this.entityCache[guid]['tags'];
1154
- entity.$putData(
1155
- this.entityCache[guid]['data'],
1156
- this.entityCache[guid]['sdata'],
1157
- 'server',
1158
- );
1159
- return entity;
1160
- }
1248
+ return {
1249
+ guid,
1250
+ cdate: this.entityCache[guid]['cdate'],
1251
+ mdate: this.entityCache[guid]['mdate'],
1252
+ tags: this.entityCache[guid]['tags'],
1253
+ data: this.entityCache[guid]['data'],
1254
+ sdata: this.entityCache[guid]['sdata'],
1255
+ };
1161
1256
  }
1162
1257
  return null;
1163
1258
  }
@@ -1194,11 +1289,11 @@ export default abstract class NymphDriver {
1194
1289
  }
1195
1290
  // Cache the entity.
1196
1291
  this.entityCache[guid] = {
1197
- cdate: cdate,
1198
- mdate: mdate,
1199
- tags: tags,
1200
- data: data,
1201
- sdata: sdata,
1292
+ cdate,
1293
+ mdate,
1294
+ tags,
1295
+ data,
1296
+ sdata,
1202
1297
  };
1203
1298
  while (
1204
1299
  this.nymph.config.cacheLimit &&
@@ -1530,6 +1530,61 @@ export function EntitiesTest(
1530
1530
  expect(resultGuid.indexOf(testEntity.guid ?? '')).toBeGreaterThan(-1);
1531
1531
  });
1532
1532
 
1533
+ it('object return', async () => {
1534
+ await createTestEntities();
1535
+
1536
+ // Testing object return using logic operations...
1537
+ const resultObjects = await nymph.getEntities(
1538
+ { class: TestModel, return: 'object' },
1539
+ {
1540
+ type: '&',
1541
+ '!ref': [
1542
+ ['refArray', guid()],
1543
+ ['refArray', guid()],
1544
+ ],
1545
+ '!lte': ['number', 29.99],
1546
+ },
1547
+ {
1548
+ type: '|',
1549
+ '!lte': [
1550
+ ['number', 29.99],
1551
+ ['number', 30],
1552
+ ],
1553
+ },
1554
+ {
1555
+ type: '!&',
1556
+ '!equal': ['string', 'test'],
1557
+ '!contain': [
1558
+ ['array', 'full'],
1559
+ ['array', 'of'],
1560
+ ['array', 'values'],
1561
+ ['array', 500],
1562
+ ['array', { test: true }],
1563
+ ['array', { nullbyte: '\\\x00\u0000 \x00' }],
1564
+ ],
1565
+ },
1566
+ {
1567
+ type: '!|',
1568
+ '!equal': ['string', 'test'],
1569
+ contain: [
1570
+ ['array', 'full'],
1571
+ ['array', 'of'],
1572
+ ['array', 'values'],
1573
+ ['array', 500],
1574
+ ],
1575
+ },
1576
+ );
1577
+
1578
+ const resultObject = resultObjects.find(
1579
+ (entity) => entity.guid === testEntity.guid,
1580
+ );
1581
+
1582
+ expect(resultObject).toBeInstanceOf(Object);
1583
+ expect(resultObject).not.toBeInstanceOf(TestModel);
1584
+ expect(Array.isArray(resultObject?.tags)).toBeTruthy();
1585
+ expect(resultObject?.string).toEqual('test');
1586
+ });
1587
+
1533
1588
  it('deep selector', async () => {
1534
1589
  await createTestEntities();
1535
1590
 
package/src/testMocks.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { guid } from '@nymphjs/guid';
2
2
 
3
3
  import type Nymph from './Nymph.js';
4
- import type { EntityInstanceType } from './Entity.js';
4
+ import type { EntityInstanceType, EntityObjectType } from './Entity.js';
5
5
  import type {
6
6
  EntityConstructor,
7
7
  EntityInterface,
@@ -24,6 +24,10 @@ export class MockNymphDriver {
24
24
  options: Options<T> & { return: 'guid' },
25
25
  ...selectors: Selector[]
26
26
  ): Promise<string | null>;
27
+ public async getEntity<T extends EntityConstructor = EntityConstructor>(
28
+ options: Options<T> & { return: 'object' },
29
+ ...selectors: Selector[]
30
+ ): Promise<EntityObjectType<T> | null>;
27
31
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
28
32
  options: Options<T>,
29
33
  ...selectors: Selector[]
@@ -32,6 +36,10 @@ export class MockNymphDriver {
32
36
  options: Options<T> & { return: 'guid' },
33
37
  guid: string,
34
38
  ): Promise<string | null>;
39
+ public async getEntity<T extends EntityConstructor = EntityConstructor>(
40
+ options: Options<T> & { return: 'object' },
41
+ guid: string,
42
+ ): Promise<EntityObjectType<T> | null>;
35
43
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
36
44
  options: Options<T>,
37
45
  guid: string,
@@ -39,7 +47,7 @@ export class MockNymphDriver {
39
47
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
40
48
  options: Options<T> = {},
41
49
  ...selectors: Selector[] | string[]
42
- ): Promise<EntityInstanceType<T> | string | null> {
50
+ ): Promise<EntityInstanceType<T> | EntityObjectType<T> | string | null> {
43
51
  const guid =
44
52
  typeof selectors[0] === 'string' ? selectors[0] : selectors[0].guid;
45
53
  if (!options || !guid || typeof guid !== 'string' || !(guid in entities)) {
@@ -117,6 +125,10 @@ export class MockNymph {
117
125
  options: Options<T> & { return: 'guid' },
118
126
  ...selectors: Selector[]
119
127
  ): Promise<string | null>;
128
+ public async getEntity<T extends EntityConstructor = EntityConstructor>(
129
+ options: Options<T> & { return: 'object' },
130
+ ...selectors: Selector[]
131
+ ): Promise<EntityObjectType<T> | null>;
120
132
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
121
133
  options: Options<T>,
122
134
  ...selectors: Selector[]
@@ -125,6 +137,10 @@ export class MockNymph {
125
137
  options: Options<T> & { return: 'guid' },
126
138
  guid: string,
127
139
  ): Promise<string | null>;
140
+ public async getEntity<T extends EntityConstructor = EntityConstructor>(
141
+ options: Options<T> & { return: 'object' },
142
+ guid: string,
143
+ ): Promise<EntityObjectType<T> | null>;
128
144
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
129
145
  options: Options<T>,
130
146
  guid: string,
@@ -132,7 +148,7 @@ export class MockNymph {
132
148
  public async getEntity<T extends EntityConstructor = EntityConstructor>(
133
149
  options: Options<T> = {},
134
150
  ...selectors: Selector[] | string[]
135
- ): Promise<EntityInstanceType<T> | string | null> {
151
+ ): Promise<EntityInstanceType<T> | EntityObjectType<T> | string | null> {
136
152
  // @ts-ignore: The selector type is correct here.
137
153
  return await this.driver.getEntity(options, ...selectors);
138
154
  }