From eff55db96db9655568442cfd47cb17964b5b0b4d Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Tue, 2 Apr 2024 08:49:48 +0300 Subject: [PATCH] Define EdmSchema annotations (#17) * define entity type annotation * add test classes * define entity type constructor * define annotations * use superagent instead of axios (#18) * use superagent instead of axios * 2.13.0 * include original error for futher processing * Add type renderer (#19) * add type renderer * render type declarations * sort exported entity types * fix build * implement client-cli * 2.14.0 * add file schema renderer (#20) * add file schema renderer * 2.14.1 * test entity annotations * 2.14.2 --- package-lock.json | 2 +- package.json | 2 +- src/client.ts | 28 +++++++++--- src/metadata.ts | 65 ++++++++++++++++++++++------ src/test/EdmSchema.spec.ts | 88 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 src/test/EdmSchema.spec.ts diff --git a/package-lock.json b/package-lock.json index f96b4d6..99a1e23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@themost/client", - "version": "2.14.1", + "version": "2.14.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e149435..cd61868 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@themost/client", - "version": "2.14.1", + "version": "2.14.2", "description": "MOST Web Framework Codename Blueshift - Client Common", "module": "dist/index.esm.js", "main": "dist/index.js", diff --git a/src/client.ts b/src/client.ts index e202f5d..6c6a06d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5,7 +5,7 @@ import {ClientDataServiceBase, ClientDataContextBase, TextUtils, DataServiceQuer configurable, enumerable, DataServiceHeaders} from './common'; -import {EdmSchema} from './metadata'; +import {EdmSchema, EntityConstructor, EntitySetAnnotation, EntityTypeAnnotation} from './metadata'; import { OpenDataQuery, OpenDataQueryFormatter, QueryFunc } from '@themost/query' import {SyncSeriesEventEmitter} from '@themost/events'; @@ -826,8 +826,23 @@ export class ClientDataModel { private readonly _name: string; - constructor(name: string, service: ClientDataServiceBase) { - this._name = name; + constructor(model: string | EntityConstructor, service: ClientDataServiceBase) { + if (model instanceof Function) { + const entitySetAnnotation = model as unknown as EntitySetAnnotation; + if (entitySetAnnotation.EntitySet) { + // get entity set name as model + const { name } = entitySetAnnotation.EntitySet; + this._name = name || model.name; + } + const entityTypeAnnotation = model as unknown as EntityTypeAnnotation; + if (entityTypeAnnotation.Entity) { + // get entity type name as model + const { name } = entityTypeAnnotation.Entity; + this._name = name || model.name; + } + } else if (typeof model === 'string') { + this._name = model; + } Object.defineProperty(this, '_service', { configurable: false, enumerable: false, @@ -1067,12 +1082,11 @@ export class ClientDataContext implements ClientDataContextBase { /** * Gets an instance of ClientDataModel class - * @param name {string|*} - A string which represents the name of the data model. + * @param model {string|*} - A string which represents the name of the data model. * @returns {ClientDataModel} */ - public model(name: string): ClientDataModel { - Args.notEmpty(name, 'Model name'); - return new ClientDataModel(name, this.service); + public model(model: string | EntityConstructor): ClientDataModel { + return new ClientDataModel(model, this.service); } public getMetadata(force = false): Promise { diff --git a/src/metadata.ts b/src/metadata.ts index 4168b41..8a68ae4 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -1,4 +1,18 @@ -import {XDocument, XNode, XSerializer} from '@themost/xml'; +import { XDocument, XNode, XSerializer } from '@themost/xml'; + +export declare type EntityConstructor = new(...args: any[]) => T; + +export declare interface EntityTypeAnnotation { + Entity: { + name?: string + } +} + +export declare interface EntitySetAnnotation { + EntitySet: { + name?: string + } +} /** * Represents an OData service metadata document @@ -19,20 +33,45 @@ export class EdmSchema { public Action: EdmAction[] = []; public Function: EdmFunction[] = []; public readXml(node: XNode) { - this.EntityType = node.selectNodes('EntityType').map( (x) => { + this.EntityType = node.selectNodes('EntityType').map((x) => { return XSerializer.deserialize(x, EdmEntityType); }); const entityContainerNode = node.selectSingleNode('EntityContainer'); if (entityContainerNode) { this.EntityContainer = XSerializer.deserialize(entityContainerNode, EdmEntityContainer); } - this.Action = node.selectNodes('Action').map( (x) => { + this.Action = node.selectNodes('Action').map((x) => { return XSerializer.deserialize(x, EdmAction); }); - this.Function = node.selectNodes('Function').map( (x) => { + this.Function = node.selectNodes('Function').map((x) => { return XSerializer.deserialize(x, EdmFunction); }); } + + static entityType(name?: string) { + return (target: EntityConstructor) => { + // get entity type + const entityType = target as unknown as EntityTypeAnnotation; + // use name or target entity name + const entityName = name || target.name; + // assign entity type annotation + entityType.Entity = Object.assign({ + name: entityName + }); + } + } + + static entitySet(name: string) { + return (target: EntityConstructor) => { + // get entity type + const entitySet = target as unknown as EntitySetAnnotation; + // assign entity type annotation + entitySet.EntitySet = Object.assign({ + name + }); + } + } + } /** @@ -41,7 +80,7 @@ export class EdmSchema { export class EdmEntityContainer { public EntitySet: EdmEntitySet[] = []; public readXml(node: XNode) { - this.EntitySet = node.selectNodes('EntitySet').map( (x) => { + this.EntitySet = node.selectNodes('EntitySet').map((x) => { return XSerializer.deserialize(x, EdmEntitySet); }); } @@ -55,7 +94,7 @@ export class EdmProcedure { public readXml(node: XNode) { this.Name = node.getAttribute('Name'); this.IsBound = node.getAttribute('IsBound') === 'true'; - this.Parameter = node.selectNodes('Parameter').map( (x) => { + this.Parameter = node.selectNodes('Parameter').map((x) => { return XSerializer.deserialize(x, EdmParameter); }); const returnTypeNode = node.selectSingleNode('ReturnType'); @@ -148,7 +187,7 @@ export class EdmProperty { if (longDescription) { this.LongDescription = longDescription.getAttribute('String'); } - this.Annotations = node.selectNodes('Annotation').map( (annotationNode) => { + this.Annotations = node.selectNodes('Annotation').map((annotationNode) => { return XSerializer.deserialize(annotationNode, EdmAnnotation); }); } @@ -187,7 +226,7 @@ export class EdmNavigationProperty { if (longDescription) { this.LongDescription = longDescription.getAttribute('String'); } - this.Annotations = node.selectNodes('Annotation').map( (annotationNode) => { + this.Annotations = node.selectNodes('Annotation').map((annotationNode) => { return XSerializer.deserialize(annotationNode, EdmAnnotation); }); } @@ -199,7 +238,7 @@ export class EdmNavigationProperty { export class EdmKey { public PropertyRef: EdmPropertyRef[] = []; public readXml(node: XNode) { - this.PropertyRef = node.selectNodes('PropertyRef').map( (x) => { + this.PropertyRef = node.selectNodes('PropertyRef').map((x) => { return XSerializer.deserialize(x, EdmPropertyRef); }); } @@ -238,10 +277,10 @@ export class EdmEntityType { if (keyNode) { this.Key = XSerializer.deserialize(keyNode, EdmKey); } - this.Property = node.selectNodes('Property').map( (x) => { + this.Property = node.selectNodes('Property').map((x) => { return XSerializer.deserialize(x, EdmProperty); }); - this.NavigationProperty = node.selectNodes('NavigationProperty').map( (x) => { + this.NavigationProperty = node.selectNodes('NavigationProperty').map((x) => { return XSerializer.deserialize(x, EdmNavigationProperty); }); // get implements annotation @@ -249,10 +288,11 @@ export class EdmEntityType { if (implementsAnnotation) { this.ImplementsType = implementsAnnotation.getAttribute('String'); } - this.Annotations = node.selectNodes('Annotation').map( (annotationNode) => { + this.Annotations = node.selectNodes('Annotation').map((annotationNode) => { return XSerializer.deserialize(annotationNode, EdmAnnotation); }); } + } /** @@ -274,6 +314,7 @@ export class EdmEntitySet { this.ResourcePath = resourcePathNode.getAttribute('String'); } } + } export class EdmAnnotation { diff --git a/src/test/EdmSchema.spec.ts b/src/test/EdmSchema.spec.ts new file mode 100644 index 0000000..9cf61da --- /dev/null +++ b/src/test/EdmSchema.spec.ts @@ -0,0 +1,88 @@ +import {EdmSchema} from '@themost/client'; +import {TestContext} from './TestUtils'; + +class Thing { + id?: number; + name?: string; + description?: string; + createdBy?: number; + modifiedBy?: number; + dateCreated?: Date; + dateModified?: Date; + sameAs?: string; + url?: string; + image?: string; + additionalType?: string; + identifier?: string; + alternateName?: string; + disambiguatingDescription?: string; +} + +@EdmSchema.entitySet('Products') +class Product extends Thing { + model?: string; + productID?: string; + category?: string; + releaseDate?: Date; + discontinued?: boolean; + price?: number; + isRelatedTo?: Product | number; + isSimilarTo?: Product | number; +} + +@EdmSchema.entityType('Order') +class Order extends Thing { + orderDate?: Date; + customer?: Person | number; + orderedItem?: Product | number; +} + +@EdmSchema.entityType() +class Person extends Thing { + +} + +describe('EdmSchema', () => { + + let context: TestContext; + beforeAll(async () => { + context = new TestContext(); + await context.authenticate(); + }); + + it('should define entity set annotation', () => { + const annotation = Product as unknown as { + EntitySet: { + name: string + } + } + expect(annotation.EntitySet).toBeTruthy(); + expect(annotation.EntitySet.name).toEqual('Products'); + }); + + it('should define entity type annotation', () => { + const annotation = Order as unknown as { + Entity: { + name: string + } + } + expect(annotation.Entity).toBeTruthy(); + expect(annotation.Entity.name).toEqual('Order'); + }); + + it('should define entity type annotation from class', () => { + const annotation = Person as unknown as { + Entity: { + name: string + } + } + expect(annotation.Entity).toBeTruthy(); + expect(annotation.Entity.name).toEqual('Person'); + }); + + it('should get items by using class', async () => { + const items = await context.model(Product).where((x) => x.category === 'Laptops').getItems(); + expect(items).toBeTruthy(); + expect(items.length).toBeTruthy(); + }); +});