UNPKG

@nymphjs/client

Version:

Nymph.js - Client

459 lines 15.9 kB
import Entity from './Entity.js'; import EntityWeakCache from './EntityWeakCache.js'; import HttpRequester, { ClientError } from './HttpRequester.js'; import { entitiesToReferences, entityConstructorsToClassNames, } from './utils.js'; let requester; export default class Nymph { /** * And optional PubSub client instance. */ pubsub = undefined; /** * A simple map of names to Entity classes. */ entityClasses = {}; /** * The entity class for this instance of Nymph. */ Entity; requestCallbacks = []; responseCallbacks = []; restUrl = ''; weakCache = false; /** * Headers that will be sent with every request. * * These are used by Tilmeld for authentication. */ headers = {}; /** * The entity cache. */ cache = new EntityWeakCache(); /** * Return `null` or empty array instead of error when entity/ies not found. */ returnNullOnNotFound = false; constructor(NymphOptions) { this.restUrl = NymphOptions.restUrl; // @ts-ignore TS doesn't know about WeakRef. this.weakCache = !!NymphOptions.weakCache && typeof WeakRef !== 'undefined'; if ('returnNullOnNotFound' in NymphOptions && NymphOptions.returnNullOnNotFound != null) { this.returnNullOnNotFound = NymphOptions.returnNullOnNotFound; } this.Entity = this.addEntityClass(Entity); requester = new HttpRequester('fetch' in NymphOptions ? NymphOptions.fetch : undefined); if ('renewTokens' in NymphOptions && !NymphOptions.renewTokens) { this.headers['X-Tilmeld-Token-Renewal'] = 'off'; } requester.on('request', (_requester, url, options) => { for (let i = 0; i < this.requestCallbacks.length; i++) { this.requestCallbacks[i] && this.requestCallbacks[i](url, options); } }); requester.on('response', (_requester, response, text) => { for (let i = 0; i < this.responseCallbacks.length; i++) { this.responseCallbacks[i] && this.responseCallbacks[i](response, text); } }); } /** * Add your class to this instance. * * This will create a class that extends your class within this instance of * Nymph and return it. You can then use this class's constructor and methods, * which will use this instance of Nymph. * * Because this creates a subclass, don't use the class returned from * `getEntityClass` to check with `instanceof`. Instead, use the base class * that you passed into this method. */ addEntityClass(entityClass) { const nymph = this; class NymphEntity extends entityClass { static nymph = nymph; constructor(...args) { super(...args); } } this.entityClasses[entityClass.class] = NymphEntity; return NymphEntity; } getEntityClass(className) { let key = null; if (typeof className === 'string') { key = className; } else { key = className.class; } if (key in this.entityClasses) { return this.entityClasses[key]; } throw new ClassNotAvailableError('Tried to use class: ' + key); } async newUID(name) { const data = await requester.POST({ url: this.restUrl, headers: { ...this.headers }, dataType: 'text', data: { action: 'uid', data: name }, }); return Number(data); } async setUID(name, value) { return await requester.PUT({ url: this.restUrl, headers: { ...this.headers }, dataType: 'json', data: { action: 'uid', data: { name, value } }, }); } async getUID(name) { const data = await requester.GET({ url: this.restUrl, headers: { ...this.headers }, dataType: 'text', data: { action: 'uid', data: name }, }); return Number(data); } async deleteUID(name) { return await requester.DELETE({ url: this.restUrl, headers: { ...this.headers }, dataType: 'text', data: { action: 'uid', data: name }, }); } async saveEntity(entity) { let method = entity.guid == null ? 'POST' : 'PUT'; return await this.requestWithMethod(entity, method, entity, false); } async saveEntities(entities) { if (!entities.length) { return Promise.resolve(false); } let method = entities[0].guid == null ? 'POST' : 'PUT'; entities.forEach((cur) => { if ((method === 'POST' && cur.guid != null) || (method === 'PUT' && cur.guid == null)) { throw new InvalidRequestError('Due to REST restriction, you can only create new entities or ' + 'update existing entities, not both at the same time.'); } }); return await this.requestWithMethod(entities, method, entities, true); } async patchEntity(entity) { if (entity.guid == null) { throw new InvalidRequestError("You can't patch an entity that hasn't yet been saved."); } let patch = entity.$getPatch(); return await this.requestWithMethod(entity, 'PATCH', patch, false); } async patchEntities(entities) { if (!entities.length) { return Promise.resolve(false); } entities.forEach((cur) => { if (cur.guid == null) { throw new InvalidRequestError('Due to REST restriction, you can only create new entities or ' + 'update existing entities, not both at the same time.'); } }); let patch = entities.map((e) => e.$getPatch()); return await this.requestWithMethod(entities, 'PATCH', patch, true); } async requestWithMethod(entity, method, data, plural) { const response = await requester[method]({ url: this.restUrl, headers: { ...this.headers }, dataType: 'json', data: { action: plural ? 'entities' : 'entity', data, }, }); if (plural && Array.isArray(entity) && entity.length === response.length) { return entity.map((e, i) => response[i] && typeof response[i].guid !== 'undefined' && (e.guid == null || e.guid === response[i].guid) ? e.$init(response[i]) : e); } else if (!Array.isArray(entity) && typeof response.guid !== 'undefined') { return entity.$init(response); } throw new Error('Server error'); } async getEntity(options, ...selectors) { let data = null; try { // @ts-ignore: Implementation signatures of overloads are not externally visible. data = (await this.getEntityData(options, ...selectors)); } catch (e) { if (this.returnNullOnNotFound && e instanceof ClientError && e.status === 404) { data = null; } else { throw e; } } if (options.return && options.return === 'count') { return Number(data ?? 0); } if (data != null) { if (options.return && options.return === 'guid') { return data; } else { return this.initEntity(data); } } return null; } async getEntityData(options, ...selectors) { if (options.class instanceof Entity) { throw new InvalidRequestError("You can't make REST requests with the base Entity class."); } // Set up options and selectors. if (typeof selectors[0] === 'string') { selectors = [{ type: '&', guid: selectors[0] }]; } const data = await requester.GET({ url: this.restUrl, headers: { ...this.headers }, dataType: 'json', data: { action: 'entity', data: [ { ...options, class: options.class.class }, ...entityConstructorsToClassNames(selectors), ], }, }); if (options.return === 'count' || typeof data.guid !== 'undefined') { return data; } return null; } async getEntities(options, ...selectors) { let data = null; try { data = await requester.GET({ url: this.restUrl, headers: { ...this.headers }, dataType: 'json', data: { action: 'entities', data: [ { ...options, class: options.class.class }, ...entityConstructorsToClassNames(selectors), ], }, }); } catch (e) { if (this.returnNullOnNotFound && e instanceof ClientError && e.status === 404) { data = null; } else { throw e; } } if (options.return && options.return === 'count') { return Number(data ?? 0); } if (data == null) { return []; } if (options.return && options.return === 'guid') { return data; } return data.map((e) => this.initEntity(e)); } initEntity(entityJSON) { const EntityClass = this.getEntityClass(entityJSON.class); if (!EntityClass) { throw new ClassNotAvailableError(entityJSON.class + ' class cannot be found.'); } let entity = EntityClass.factorySync(); if (this.weakCache) { // Try to get it from cache. const entityFromCache = this.cache.get(EntityClass, entityJSON.guid || ''); if (entityFromCache != null) { entity = entityFromCache; } } return entity.$init(entityJSON); } getEntityFromCache(EntityClass, guid) { if (!this.weakCache) { return null; } return this.cache.get(EntityClass, guid); } setEntityToCache(EntityClass, entity) { if (!this.weakCache) { return; } return this.cache.set(EntityClass, entity); } initEntitiesFromData(item) { if (Array.isArray(item)) { // Recurse into lower arrays. return item.map((entry) => this.initEntitiesFromData(entry)); } else if (item instanceof Object && !(item instanceof Entity)) { if (item.hasOwnProperty('class') && item.hasOwnProperty('guid') && item.hasOwnProperty('cdate') && item.hasOwnProperty('mdate') && item.hasOwnProperty('tags') && item.hasOwnProperty('data') && this.getEntityClass(item.class)) { return this.initEntity(item); } else { for (let [key, value] of Object.entries(item)) { // @ts-ignore: Key comes from the object. It's fine. item[key] = this.initEntitiesFromData(value); } } } // Not an entity or array, just return it. return item; } async deleteEntity(entity, _plural = false) { return await requester.DELETE({ url: this.restUrl, headers: { ...this.headers }, dataType: 'json', data: { action: _plural ? 'entities' : 'entity', data: _plural && Array.isArray(entity) ? entity.map((e) => ({ guid: e.guid, class: e.constructor.class, })) : { guid: entity.guid, class: entity.constructor.class, }, }, }); } async deleteEntities(entities) { return await this.deleteEntity(entities, true); } async serverCall(entity, method, params, stateless = false) { const data = await requester.POST({ url: this.restUrl, headers: { ...this.headers }, dataType: 'json', data: { action: 'method', data: { entity, stateless, method, params: entitiesToReferences(entityConstructorsToClassNames(params)), }, }, }); return { ...data, return: this.initEntitiesFromData(data.return), }; } async serverCallStatic(className, method, params) { const data = await requester.POST({ url: this.restUrl, headers: { ...this.headers }, dataType: 'json', data: { action: 'method', data: { class: className, static: true, method: method, params: entitiesToReferences(entityConstructorsToClassNames(params)), }, }, }); return this.initEntitiesFromData(data); } async serverCallStaticIterator(className, method, params) { const iterable = await requester.POST_ITERATOR({ url: this.restUrl, headers: { ...this.headers }, dataType: 'json', data: { action: 'method', data: { class: className, static: true, method: method, iterator: true, params: entitiesToReferences(entityConstructorsToClassNames(params)), }, }, }); const that = this; const iterator = { abortController: iterable.abortController, async *[Symbol.asyncIterator]() { for await (let response of iterable) { if (response instanceof Error) { yield response; } else { yield that.initEntitiesFromData(response); } } }, }; return iterator; } on(event, callback) { const prop = (event + 'Callbacks'); if (!(prop in this)) { throw new Error('Invalid event type.'); } // @ts-ignore: The callback should always be the right type here. this[prop].push(callback); return () => this.off(event, callback); } off(event, callback) { const prop = (event + 'Callbacks'); if (!(prop in this)) { return false; } // @ts-ignore: The callback should always be the right type here. const i = this[prop].indexOf(callback); if (i > -1) { // @ts-ignore: The callback should always be the right type here. this[prop].splice(i, 1); } return true; } } export class ClassNotAvailableError extends Error { constructor(message) { super(message); this.name = 'ClassNotAvailableError'; } } export class InvalidRequestError extends Error { constructor(message) { super(message); this.name = 'InvalidRequestError'; } } //# sourceMappingURL=Nymph.js.map