1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import {AnyObject, DataObject, Options, PrototypeOf} from './common-types';
|
7 | import {
|
8 | BelongsToDefinition,
|
9 | HasManyDefinition,
|
10 | HasOneDefinition,
|
11 | JsonSchema,
|
12 | ReferencesManyDefinition,
|
13 | RelationMetadata,
|
14 | RelationType,
|
15 | } from './index';
|
16 | import {TypeResolver} from './type-resolver';
|
17 | import {Type} from './types';
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | export interface JsonSchemaWithExtensions extends JsonSchema {
|
28 | [attributes: string]: any;
|
29 | }
|
30 |
|
31 | export type PropertyType =
|
32 | | string
|
33 | | Function
|
34 | | object
|
35 | | Type<any>
|
36 | | TypeResolver<Model>;
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | export interface PropertyDefinition {
|
42 | type: PropertyType;
|
43 | id?: boolean | number;
|
44 | |
45 |
|
46 |
|
47 |
|
48 | hidden?: boolean;
|
49 | json?: PropertyForm;
|
50 | jsonSchema?: JsonSchemaWithExtensions;
|
51 | store?: PropertyForm;
|
52 | itemType?: PropertyType;
|
53 | [attribute: string]: any;
|
54 | }
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | export interface ModelSettings {
|
61 | |
62 |
|
63 |
|
64 | description?: string;
|
65 | |
66 |
|
67 |
|
68 | forceId?: boolean;
|
69 | |
70 |
|
71 |
|
72 | hiddenProperties?: string[];
|
73 | |
74 |
|
75 |
|
76 | scope?: object;
|
77 | |
78 |
|
79 |
|
80 | strict?: boolean | 'filter';
|
81 |
|
82 |
|
83 | [name: string]: any;
|
84 | }
|
85 |
|
86 |
|
87 |
|
88 |
|
89 | export interface PropertyForm {
|
90 | in?: boolean;
|
91 | out?: boolean;
|
92 | name?: string;
|
93 | }
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 | export type RelationDefinitionMap = {
|
100 | [relationName: string]: RelationMetadata;
|
101 | };
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | export interface ModelDefinitionSyntax {
|
107 | name: string;
|
108 | properties?: {[name: string]: PropertyDefinition | PropertyType};
|
109 | settings?: ModelSettings;
|
110 | relations?: RelationDefinitionMap;
|
111 | jsonSchema?: JsonSchemaWithExtensions;
|
112 | [attribute: string]: any;
|
113 | }
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | export class ModelDefinition {
|
119 | readonly name: string;
|
120 | properties: {[name: string]: PropertyDefinition};
|
121 | settings: ModelSettings;
|
122 | relations: RelationDefinitionMap;
|
123 |
|
124 | [attribute: string]: any;
|
125 |
|
126 | constructor(nameOrDef: string | ModelDefinitionSyntax) {
|
127 | if (typeof nameOrDef === 'string') {
|
128 | nameOrDef = {name: nameOrDef};
|
129 | }
|
130 | const {name, properties, settings, relations} = nameOrDef;
|
131 |
|
132 | this.name = name;
|
133 |
|
134 | this.properties = {};
|
135 | if (properties) {
|
136 | for (const p in properties) {
|
137 | this.addProperty(p, properties[p]);
|
138 | }
|
139 | }
|
140 |
|
141 | this.settings = settings ?? new Map();
|
142 | this.relations = relations ?? {};
|
143 | }
|
144 |
|
145 | |
146 |
|
147 |
|
148 |
|
149 |
|
150 | addProperty(
|
151 | name: string,
|
152 | definitionOrType: PropertyDefinition | PropertyType,
|
153 | ): this {
|
154 | const definition = (definitionOrType as PropertyDefinition).type
|
155 | ? (definitionOrType as PropertyDefinition)
|
156 | : {type: definitionOrType};
|
157 |
|
158 | if (
|
159 | definition.id === true &&
|
160 | definition.generated === true &&
|
161 | definition.type !== undefined &&
|
162 | definition.useDefaultIdType === undefined
|
163 | ) {
|
164 | definition.useDefaultIdType = false;
|
165 | }
|
166 |
|
167 | this.properties[name] = definition;
|
168 | return this;
|
169 | }
|
170 |
|
171 | |
172 |
|
173 |
|
174 |
|
175 |
|
176 | addSetting(name: string, value: any): this {
|
177 | this.settings[name] = value;
|
178 | return this;
|
179 | }
|
180 |
|
181 | |
182 |
|
183 |
|
184 |
|
185 | addRelation(definition: RelationMetadata): this {
|
186 | this.relations[definition.name] = definition;
|
187 | return this;
|
188 | }
|
189 |
|
190 | |
191 |
|
192 |
|
193 |
|
194 |
|
195 | belongsTo(
|
196 | name: string,
|
197 | definition: Omit<BelongsToDefinition, 'name' | 'type' | 'targetsMany'>,
|
198 | ): this {
|
199 | const meta: BelongsToDefinition = {
|
200 | ...definition,
|
201 | name,
|
202 | type: RelationType.belongsTo,
|
203 | targetsMany: false,
|
204 | };
|
205 | return this.addRelation(meta);
|
206 | }
|
207 |
|
208 | |
209 |
|
210 |
|
211 |
|
212 |
|
213 | hasOne(
|
214 | name: string,
|
215 | definition: Omit<HasOneDefinition, 'name' | 'type' | 'targetsMany'>,
|
216 | ): this {
|
217 | const meta: HasOneDefinition = {
|
218 | ...definition,
|
219 | name,
|
220 | type: RelationType.hasOne,
|
221 | targetsMany: false,
|
222 | };
|
223 | return this.addRelation(meta);
|
224 | }
|
225 |
|
226 | |
227 |
|
228 |
|
229 |
|
230 |
|
231 | hasMany(
|
232 | name: string,
|
233 | definition: Omit<HasManyDefinition, 'name' | 'type' | 'targetsMany'>,
|
234 | ): this {
|
235 | const meta: HasManyDefinition = {
|
236 | ...definition,
|
237 | name,
|
238 | type: RelationType.hasMany,
|
239 | targetsMany: true,
|
240 | };
|
241 | return this.addRelation(meta);
|
242 | }
|
243 |
|
244 | |
245 |
|
246 |
|
247 |
|
248 |
|
249 | referencesMany(
|
250 | name: string,
|
251 | definition: Omit<ReferencesManyDefinition, 'name' | 'type' | 'targetsMany'>,
|
252 | ): this {
|
253 | const meta: ReferencesManyDefinition = {
|
254 | ...definition,
|
255 | name,
|
256 | type: RelationType.referencesMany,
|
257 | targetsMany: true,
|
258 | };
|
259 | return this.addRelation(meta);
|
260 | }
|
261 |
|
262 | |
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 | idProperties(): string[] {
|
282 | if (typeof this.settings.id === 'string') {
|
283 | return [this.settings.id];
|
284 | } else if (Array.isArray(this.settings.id)) {
|
285 | return this.settings.id;
|
286 | }
|
287 | const idProps = Object.keys(this.properties).filter(
|
288 | prop => this.properties[prop].id,
|
289 | );
|
290 | return idProps;
|
291 | }
|
292 | }
|
293 |
|
294 | function asJSON(value: any): any {
|
295 | if (value == null) return value;
|
296 | if (typeof value.toJSON === 'function') {
|
297 | return value.toJSON();
|
298 | }
|
299 |
|
300 | if (Array.isArray(value)) {
|
301 | return value.map(item => asJSON(item));
|
302 | }
|
303 | return value;
|
304 | }
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 | function asObject(value: any, options?: Options): any {
|
318 | if (value == null) return value;
|
319 | if (typeof value.toObject === 'function') {
|
320 | return value.toObject(options);
|
321 | }
|
322 | if (Array.isArray(value)) {
|
323 | return value.map(item => asObject(item, options));
|
324 | }
|
325 | return value;
|
326 | }
|
327 |
|
328 |
|
329 |
|
330 |
|
331 | export class Model {
|
332 | static get modelName(): string {
|
333 | return this.definition?.name || this.name;
|
334 | }
|
335 |
|
336 | static definition: ModelDefinition;
|
337 |
|
338 | |
339 |
|
340 |
|
341 | toJSON(): Object {
|
342 | const def = (this.constructor as typeof Model).definition;
|
343 | if (def == null || def.settings.strict === false) {
|
344 | return this.toObject({ignoreUnknownProperties: false});
|
345 | }
|
346 |
|
347 | const copyPropertyAsJson = (key: string) => {
|
348 | const val = asJSON((this as AnyObject)[key]);
|
349 | if (val !== undefined) {
|
350 | json[key] = val;
|
351 | }
|
352 | };
|
353 |
|
354 | const json: AnyObject = {};
|
355 | const hiddenProperties: string[] = def.settings.hiddenProperties || [];
|
356 | for (const p in def.properties) {
|
357 | if (p in this && !hiddenProperties.includes(p)) {
|
358 | copyPropertyAsJson(p);
|
359 | }
|
360 | }
|
361 |
|
362 | for (const r in def.relations) {
|
363 | const relName = def.relations[r].name;
|
364 | if (relName in this) {
|
365 | copyPropertyAsJson(relName);
|
366 | }
|
367 | }
|
368 |
|
369 | return json;
|
370 | }
|
371 |
|
372 | |
373 |
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 |
|
380 |
|
381 | toObject(options?: Options): Object {
|
382 | const def = (this.constructor as typeof Model).definition;
|
383 | const obj: AnyObject = {};
|
384 |
|
385 | if (options?.ignoreUnknownProperties === false) {
|
386 | const hiddenProperties: string[] = def?.settings.hiddenProperties || [];
|
387 | for (const p in this) {
|
388 | if (!hiddenProperties.includes(p)) {
|
389 | const val = (this as AnyObject)[p];
|
390 | obj[p] = asObject(val, options);
|
391 | }
|
392 | }
|
393 | return obj;
|
394 | }
|
395 |
|
396 | if (def?.relations) {
|
397 | for (const r in def.relations) {
|
398 | const relName = def.relations[r].name;
|
399 | if (relName in this) {
|
400 | obj[relName] = asObject((this as AnyObject)[relName], {
|
401 | ...options,
|
402 | ignoreUnknownProperties: false,
|
403 | });
|
404 | }
|
405 | }
|
406 | }
|
407 |
|
408 | const props = def.properties;
|
409 | const keys = Object.keys(props);
|
410 |
|
411 | for (const i in keys) {
|
412 | const propertyName = keys[i];
|
413 | const val = (this as AnyObject)[propertyName];
|
414 |
|
415 | if (val === undefined) continue;
|
416 | obj[propertyName] = asObject(val, options);
|
417 | }
|
418 |
|
419 | return obj;
|
420 | }
|
421 |
|
422 | constructor(data?: DataObject<Model>) {
|
423 | Object.assign(this, data);
|
424 | }
|
425 | }
|
426 |
|
427 | export interface Persistable {
|
428 |
|
429 | }
|
430 |
|
431 |
|
432 |
|
433 |
|
434 |
|
435 | export abstract class ValueObject extends Model implements Persistable {}
|
436 |
|
437 |
|
438 |
|
439 |
|
440 | export class Entity extends Model implements Persistable {
|
441 | |
442 |
|
443 |
|
444 | static getIdProperties(): string[] {
|
445 | return this.definition.idProperties();
|
446 | }
|
447 |
|
448 | |
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 | static getIdOf(entityOrData: AnyObject): any {
|
455 | if (typeof entityOrData.getId === 'function') {
|
456 | return entityOrData.getId();
|
457 | }
|
458 |
|
459 | const idName = this.getIdProperties()[0];
|
460 | return entityOrData[idName];
|
461 | }
|
462 |
|
463 | |
464 |
|
465 |
|
466 |
|
467 | getId(): any {
|
468 | const definition = (this.constructor as typeof Entity).definition;
|
469 | const idProps = definition.idProperties();
|
470 | if (idProps.length === 1) {
|
471 | return (this as AnyObject)[idProps[0]];
|
472 | }
|
473 | if (!idProps.length) {
|
474 | throw new Error(
|
475 | `Invalid Entity ${this.constructor.name}:` +
|
476 | 'missing primary key (id) property',
|
477 | );
|
478 | }
|
479 | return this.getIdObject();
|
480 | }
|
481 |
|
482 | |
483 |
|
484 |
|
485 |
|
486 | getIdObject(): Object {
|
487 | const definition = (this.constructor as typeof Entity).definition;
|
488 | const idProps = definition.idProperties();
|
489 | const idObj = {} as any;
|
490 | for (const idProp of idProps) {
|
491 | idObj[idProp] = (this as AnyObject)[idProp];
|
492 | }
|
493 | return idObj;
|
494 | }
|
495 |
|
496 | |
497 |
|
498 |
|
499 |
|
500 | static buildWhereForId(id: any) {
|
501 | const where = {} as any;
|
502 | const idProps = this.definition.idProperties();
|
503 | if (idProps.length === 1) {
|
504 | where[idProps[0]] = id;
|
505 | } else {
|
506 | for (const idProp of idProps) {
|
507 | where[idProp] = id[idProp];
|
508 | }
|
509 | }
|
510 | return where;
|
511 | }
|
512 | }
|
513 |
|
514 |
|
515 |
|
516 |
|
517 | export class Event {
|
518 | source: any;
|
519 | type: string;
|
520 | }
|
521 |
|
522 | export type EntityData = DataObject<Entity>;
|
523 |
|
524 | export type EntityResolver<T extends Entity> = TypeResolver<T, typeof Entity>;
|
525 |
|
526 |
|
527 |
|
528 |
|
529 |
|
530 |
|
531 |
|
532 |
|
533 |
|
534 | export function rejectNavigationalPropertiesInData<M extends typeof Entity>(
|
535 | modelClass: M,
|
536 | data: DataObject<PrototypeOf<M>>,
|
537 | ) {
|
538 | const def = modelClass.definition;
|
539 | const props = def.properties;
|
540 |
|
541 | for (const r in def.relations) {
|
542 | const relName = def.relations[r].name;
|
543 | if (!(relName in data)) continue;
|
544 |
|
545 | let msg =
|
546 | 'Navigational properties are not allowed in model data ' +
|
547 | `(model "${modelClass.modelName}" property "${relName}"), ` +
|
548 | 'please remove it.';
|
549 |
|
550 | if (relName in props) {
|
551 | msg +=
|
552 | ' The error might be invoked by belongsTo relations, please make' +
|
553 | ' sure the relation name is not the same as the property name.';
|
554 | }
|
555 |
|
556 | throw new Error(msg);
|
557 | }
|
558 | }
|