UNPKG

@data-client/endpoint

Version:

Declarative Network Interface Definitions

1,196 lines (1,116 loc) 38.9 kB
'use strict'; /** * Helpers to enable Immutable compatibility *without* bringing in * the 'immutable' package as a dependency. */ /** * Check if an object is immutable by checking if it has a key specific * to the immutable library. * * @param {any} object * @return {bool} */ function isImmutable(object) { return !!(typeof object.hasOwnProperty === 'function' && (Object.hasOwnProperty.call(object, '__ownerID') || // Immutable.Map object._map && Object.hasOwnProperty.call(object._map, '__ownerID'))); // Immutable.Record } /** * Denormalize an immutable entity. * * @param {Schema} schema * @param {Immutable.Map|Immutable.Record} input * @param {function} unvisit * @param {function} getDenormalizedEntity * @return {Immutable.Map|Immutable.Record} */ function denormalizeImmutable(schema, input, unvisit) { let deleted; const value = Object.keys(schema).reduce((object, key) => { // Immutable maps cast keys to strings on write so we need to ensure // we're accessing them using string keys. const stringKey = `${key}`; const item = unvisit(schema[stringKey], object.get(stringKey)); if (typeof item === 'symbol') { deleted = item; } if (object.has(stringKey)) { return object.set(stringKey, item); } else { return object; } }, input); return deleted != null ? deleted : value; } class PolymorphicSchema { schema; constructor(definition, schemaAttribute) { if (schemaAttribute) { this._schemaAttribute = typeof schemaAttribute === 'string' ? input => input[schemaAttribute] : schemaAttribute; } this.define(definition); } get isSingleSchema() { return !this._schemaAttribute; } define(definition) { // sending Union into another Polymorphic gets hoisted if ('_schemaAttribute' in definition && !this._schemaAttribute) { this.schema = definition.schema; this._schemaAttribute = definition._schemaAttribute; } else { this.schema = definition; } } getSchemaAttribute(input, parent, key) { return !this.isSingleSchema && this._schemaAttribute(input, parent, key); } inferSchema(input, parent, key) { if (this.isSingleSchema) { return this.schema; } const attr = this.getSchemaAttribute(input, parent, key); return this.schema[attr]; } schemaKey() { if (this.isSingleSchema) { return this.schema.key; } return Object.values(this.schema).join(';'); } normalizeValue(value, parent, key, args, visit) { if (!value) return value; const schema = this.inferSchema(value, parent, key); if (!schema) { /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { const attr = this.getSchemaAttribute(value, parent, key); console.warn(`Schema attribute ${JSON.stringify(attr, undefined, 2)} is not expected. Expected one of: ${Object.keys(this.schema).map(k => `"${k}"`).join(', ')} Value: ${JSON.stringify(value, undefined, 2)}`); } return value; } const normalizedValue = visit(schema, value, parent, key, args); return this.isSingleSchema || normalizedValue === undefined || normalizedValue === null ? normalizedValue : { id: normalizedValue, schema: this.getSchemaAttribute(value, parent, key) }; } // value is guaranteed by caller to not be null denormalizeValue(value, unvisit) { const schemaKey = !this.isSingleSchema && value && (isImmutable(value) ? value.get('schema') : value.schema); if (!this.isSingleSchema && !schemaKey) { // denormalize should also handle 'passthrough' values (not normalized) and still // construct the correct Entity instance if (typeof value === 'object' && value !== null) { const schema = this.inferSchema(value, undefined, undefined); if (schema) return unvisit(schema, value); } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && value) { console.warn(`TypeError: Unable to infer schema for ${this.constructor.name} Value: ${JSON.stringify(value, undefined, 2)}.`); } return value; } const id = this.isSingleSchema ? undefined : isImmutable(value) ? value.get('id') : value.id; const schema = this.isSingleSchema ? this.schema : this.schema[schemaKey]; return unvisit(schema, id || value); } } /** * Represents polymorphic values. * @see https://dataclient.io/rest/api/Union */ class UnionSchema extends PolymorphicSchema { constructor(definition, schemaAttribute) { if (!schemaAttribute) { throw new Error('Expected option "schemaAttribute" not found on UnionSchema.'); } super(definition, schemaAttribute); } normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop) { return this.normalizeValue(input, parent, key, args, visit); } denormalize(input, args, unvisit) { return this.denormalizeValue(input, unvisit); } queryKey(args, queryKey, getEntity, getIndex) { if (!args[0]) return; const schema = this.getSchemaAttribute(args[0], undefined, ''); const discriminatedSchema = this.schema[schema]; // Was unable to infer the entity's schema from params if (discriminatedSchema === undefined) return; const id = queryKey(discriminatedSchema, args, getEntity, getIndex); if (id === undefined) return; return { id, schema }; } } /** * Represents variably sized objects * @see https://dataclient.io/rest/api/Values */ class ValuesSchema extends PolymorphicSchema { normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop) { return Object.keys(input).reduce((output, key, index) => { const value = input[key]; return value !== undefined && value !== null ? { ...output, [key]: this.normalizeValue(value, input, key, args, visit) } : output; }, {}); } denormalize(input, args, unvisit) { return Object.keys(input).reduce((output, key) => { const entityOrId = input[key]; const value = this.denormalizeValue(entityOrId, unvisit); // remove empty or deleted values if (!value || typeof value === 'symbol') return output; return { ...output, [key]: value }; }, {}); } queryKey(args, queryKey, getEntity, getIndex) { return undefined; } } const getValues = input => Array.isArray(input) ? input : Object.keys(input).map(key => input[key]); const filterEmpty = item => item !== undefined && typeof item !== 'symbol'; /** * Represents arrays * @see https://dataclient.io/rest/api/Array */ class ArraySchema extends PolymorphicSchema { normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop) { const values = getValues(input); return values.map((value, index) => this.normalizeValue(value, parent, key, args, visit)); } denormalize(input, args, unvisit) { return input.map ? input.map(entityOrId => this.denormalizeValue(entityOrId, unvisit)).filter(filterEmpty) : input; } queryKey(args, queryKey, getEntity, getIndex) { return undefined; } toJSON() { return [this.schema]; } } const INVALID = Symbol('INVALID'); /** * Retrieves all entities in cache * * @see https://dataclient.io/rest/api/All */ class AllSchema extends ArraySchema { constructor(definition, schemaAttribute) { super(definition, schemaAttribute); } normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop) { // we return undefined super.normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop); } queryKey(args, queryKey, getEntity, getIndex) { if (this.isSingleSchema) { const entitiesEntry = getEntity(this.schema.key); // we must wait until there are entries for any 'All' query to be Valid if (entitiesEntry === undefined) return INVALID; return Object.values(entitiesEntry).map(entity => entity && this.schema.pk(entity)); } let found = false; const list = Object.values(this.schema).flatMap(schema => { const entitiesEntry = getEntity(schema.key); if (entitiesEntry === undefined) return []; found = true; return Object.values(entitiesEntry).map(entity => ({ id: entity && schema.pk(entity), schema: this.getSchemaAttribute(entity, undefined, undefined) })); }); // we need at least one table entry of the Union for this to count as Valid. if (!found) return INVALID; return list; } } const normalize = (schema, input, parent, key, args, visit, addEntity, getEntity, checkLoop) => { const object = { ...input }; Object.keys(schema).forEach(key => { const localSchema = schema[key]; const value = visit(localSchema, input[key], input, key, args); if (value === undefined) { delete object[key]; } else { object[key] = value; } }); return object; }; function denormalize$1(schema, input, args, unvisit) { if (isImmutable(input)) { return denormalizeImmutable(schema, input, unvisit); } const object = { ...input }; for (const key of Object.keys(schema)) { const item = unvisit(schema[key], object[key]); if (object[key] !== undefined) { object[key] = item; } if (typeof item === 'symbol') { return item; } } return object; } function objectQueryKey(schema, args, queryKey, getEntity, getIndex) { const resultObject = {}; Object.keys(schema).forEach(k => { resultObject[k] = queryKey(schema[k], args, getEntity, getIndex); }); return resultObject; } /** * Represents objects with statically known members * @see https://dataclient.io/rest/api/Object */ class ObjectSchema { schema; constructor(definition) { this.define(definition); } define(definition) { this.schema = Object.keys(definition).reduce((entitySchema, key) => { const schema = definition[key]; return { ...entitySchema, [key]: schema }; }, this.schema || {}); } normalize(...args) { return normalize(this.schema, ...args); } denormalize(input, args, unvisit) { return denormalize$1(this.schema, input, args, unvisit); } queryKey(args, queryKey, getEntity, getIndex) { return objectQueryKey(this.schema, args, queryKey, getEntity, getIndex); } } /** * Marks entity as Invalid. * * This triggers suspense for all endpoints requiring it. * Optional (like variable sized Array and Values) will simply remove the item. * @see https://dataclient.io/rest/api/Invalidate */ class Invalidate { /** * Marks entity as Invalid. * * This triggers suspense for all endpoints requiring it. * Optional (like variable sized Array and Values) will simply remove the item. * @see https://dataclient.io/rest/api/Invalidate */ constructor(entity) { if (process.env.NODE_ENV !== 'production' && !entity) { throw new Error('Expected option "entity" not found on DeleteSchema.'); } this._entity = entity; } get key() { return this._entity.key; } /** Normalize lifecycles **/ normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop) { // TODO: what's store needs to be a differing type from fromJS const processedEntity = this._entity.process(input, parent, key, args); const id = this._entity.pk(processedEntity, parent, key, args); if (process.env.NODE_ENV !== 'production' && (id === undefined || id === '' || id === 'undefined')) { const error = new Error(`Missing usable primary key when normalizing response. This is likely due to a malformed response. Try inspecting the network response or fetch() return value. Or use debugging tools: https://dataclient.io/docs/getting-started/debugging Learn more about schemas: https://dataclient.io/docs/api/schema Invalidate(Entity): Invalidate(${this._entity.key}) Value (processed): ${input && JSON.stringify(input, null, 2)} `); error.status = 400; throw error; } addEntity(this, INVALID, id); return id; } /* istanbul ignore next */ merge(existing, incoming) { return incoming; } mergeWithStore(existingMeta, incomingMeta, existing, incoming) { // any queued updates are meaningless with delete, so we should just set it return this.merge(existing, incoming); } mergeMetaWithStore(existingMeta, incomingMeta, existing, incoming) { return incomingMeta; } /** /End Normalize lifecycles **/ queryKey(args, queryKey, getEntity, getIndex) { return undefined; } denormalize(id, args, unvisit) { return unvisit(this._entity, id); } /* istanbul ignore next */ _denormalizeNullable() { return {}; } /* istanbul ignore next */ _normalizeNullable() { return {}; } } /** This serializes in consistent way even if members are added in differnet orders */ function consistentSerialize(obj) { const keys = Object.keys(obj).sort(); const sortedObj = {}; for (const key of keys) { sortedObj[key] = obj[key]; } return JSON.stringify(sortedObj); } const pushMerge = (existing, incoming) => { return [...existing, ...incoming]; }; const unshiftMerge = (existing, incoming) => { return [...incoming, ...existing]; }; const valuesMerge = (existing, incoming) => { return { ...existing, ...incoming }; }; const createArray = value => [...value]; const createValue = value => ({ ...value }); /** * Entities but for Arrays instead of classes * @see https://dataclient.io/rest/api/Collection */ class CollectionSchema { addWith(merge, createCollectionFilter) { return CreateAdder(this, merge, createCollectionFilter); } // this adds to any list *in store* that has same members as the urlParams // so fetch(create, { userId: 'bob', completed: true }, data) // would possibly add to {}, {userId: 'bob'}, {completed: true}, {userId: 'bob', completed: true } - but only those already in the store // it ignores keys that start with sort as those are presumed to not filter results createCollectionFilter(...args) { return collectionKey => Object.entries(collectionKey).every(([key, value]) => { var _args$; return this.nonFilterArgumentKeys(key) || // strings are canonical form. See pk() above for value transformation `${args[0][key]}` === value || `${(_args$ = args[1]) == null ? void 0 : _args$[key]}` === value; }); } nonFilterArgumentKeys(key) { return key.startsWith('order'); } constructor(schema, options) { this.schema = Array.isArray(schema) ? new ArraySchema(schema[0]) : schema; if (!options) { this.argsKey = params => ({ ...params }); } else { if ('nestKey' in options) { this.nestKey = options.nestKey; } else if ('argsKey' in options) { this.argsKey = options.argsKey; } else { this.argsKey = params => ({ ...params }); } } this.key = keyFromSchema(this.schema); if (options != null && options.nonFilterArgumentKeys) { const { nonFilterArgumentKeys } = options; if (typeof nonFilterArgumentKeys === 'function') { this.nonFilterArgumentKeys = nonFilterArgumentKeys; } else if (nonFilterArgumentKeys instanceof RegExp) { this.nonFilterArgumentKeys = key => nonFilterArgumentKeys.test(key); } else { this.nonFilterArgumentKeys = key => nonFilterArgumentKeys.includes(key); } } else if (options != null && options.createCollectionFilter) // TODO(breaking): rename to filterCollections this.createCollectionFilter = options.createCollectionFilter.bind(this); // >>>>>>>>>>>>>>CREATION<<<<<<<<<<<<<< if (this.schema instanceof ArraySchema) { this.createIfValid = createArray; this.push = CreateAdder(this, pushMerge); this.unshift = CreateAdder(this, unshiftMerge); } else if (schema instanceof ValuesSchema) { this.createIfValid = createValue; this.assign = CreateAdder(this, valuesMerge); } } get cacheWith() { return this.schema.schema; } toString() { return this.key; } toJSON() { return { key: this.key, schema: this.schema.schema.toJSON() }; } pk(value, parent, key, args) { const obj = this.argsKey ? this.argsKey(...args) : this.nestKey(parent, key); for (const key in obj) { if (['number', 'boolean'].includes(typeof obj[key])) obj[key] = `${obj[key]}`; } return consistentSerialize(obj); } // >>>>>>>>>>>>>>NORMALIZE<<<<<<<<<<<<<< normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop) { const normalizedValue = this.schema.normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop); const id = this.pk(normalizedValue, parent, key, args); addEntity(this, normalizedValue, id); return id; } // always replace merge(existing, incoming) { return incoming; } shouldReorder(existingMeta, incomingMeta, existing, incoming) { return incomingMeta.fetchedAt < existingMeta.fetchedAt; } mergeWithStore(existingMeta, incomingMeta, existing, incoming) { return this.shouldReorder(existingMeta, incomingMeta, existing, incoming) ? this.merge(incoming, existing) : this.merge(existing, incoming); } mergeMetaWithStore(existingMeta, incomingMeta, existing, incoming) { return this.shouldReorder(existingMeta, incomingMeta, existing, incoming) ? existingMeta : incomingMeta; } // >>>>>>>>>>>>>>DENORMALIZE<<<<<<<<<<<<<< queryKey(args, queryKey, getEntity, getIndex) { if (this.argsKey) { const id = this.pk(undefined, undefined, '', args); // ensure this actually has entity or we shouldn't try to use it in our query if (getEntity(this.key, id)) return id; } } denormalize(input, args, unvisit) { return this.schema.denormalize(input, args, unvisit); } } function CreateAdder(collection, merge, createCollectionFilter) { const properties = { merge: { value: merge }, normalize: { value: normalizeCreate }, queryKey: { value: queryKeyCreate } }; if (collection.schema instanceof ArraySchema) { properties.createIfValid = { value: createIfValid }; properties.denormalize = { value: denormalize }; } if (createCollectionFilter) { properties.createCollectionFilter = { value: createCollectionFilter }; } return Object.create(collection, properties); } function queryKeyCreate() {} function normalizeCreate(input, parent, key, args, visit, addEntity, getEntity, checkLoop) { if (process.env.NODE_ENV !== 'production') { // means 'this is a creation endpoint' - so real PKs are not required // this is used by Entity.normalize() to determine whether to allow empty pks // visit instances are created on each normalize call so this will safely be reset visit.creating = true; } const normalizedValue = this.schema.normalize(!(this.schema instanceof ArraySchema) || Array.isArray(input) ? input : [input], parent, key, args, visit, addEntity, getEntity, checkLoop); // parent is args when not nested const filterCollections = this.createCollectionFilter(...args); // add to any collections that match this const entities = getEntity(this.key); if (entities) Object.keys(entities).forEach(collectionPk => { if (!filterCollections(JSON.parse(collectionPk))) return; addEntity(this, normalizedValue, collectionPk); }); return normalizedValue; } function createIfValid(value) { return Array.isArray(value) ? [...value] : { ...value }; } // only for arrays function denormalize(input, args, unvisit) { return Array.isArray(input) ? this.schema.denormalize(input, args, unvisit) : this.schema.denormalize([input], args, unvisit)[0]; } /** * We call schema.denormalize and schema.normalize directly * instead of visit/unvisit as we are not operating on new data * so the additional checks in those methods are redundant */ function keyFromSchema(schema) { if (schema instanceof ArraySchema) { // this assumes the definition of Array/Values is Entity return `[${schema.schemaKey()}]`; } else if (schema instanceof ValuesSchema) { return `{${schema.schemaKey()}}`; } return `(${schema.schemaKey()})`; } /** * Turns any class into an Entity. * @see https://dataclient.io/rest/api/EntityMixin */ // id is in Instance, so we default to that as pk // pk was specified in options, so we don't need to redefine function EntityMixin(Base, options = {}) { /** * Entity defines a single (globally) unique object. * @see https://dataclient.io/rest/api/Entity */ class EntityMixin extends Base { static toString() { return this.key; } static toJSON() { return { key: this.key, schema: this.schema }; } /** Defines nested entities */ /** * A unique identifier for each Entity * * @see https://dataclient.io/rest/api/Entity#pk * @param [parent] When normalizing, the object which included the entity * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ /** Returns the globally unique identifier for the static Entity */ // default implementation in class static block at bottom of definition /** Defines indexes to enable lookup by */ /** * A unique identifier for each Entity * * @see https://dataclient.io/rest/api/Entity#pk * @param [value] POJO of the entity or subset used * @param [parent] When normalizing, the object which included the entity * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ static pk(value, parent, key, args) { return this.prototype.pk.call(value, parent, key, args); } /** Return true to merge incoming data; false keeps existing entity * * @see https://dataclient.io/rest/api/Entity#shouldUpdate */ static shouldUpdate(existingMeta, incomingMeta, existing, incoming) { return true; } /** Determines the order of incoming entity vs entity already in store\ * * @see https://dataclient.io/rest/api/Entity#shouldReorder * @returns true if incoming entity should be first argument of merge() */ static shouldReorder(existingMeta, incomingMeta, existing, incoming) { return incomingMeta.fetchedAt < existingMeta.fetchedAt; } /** Creates new instance copying over defined values of arguments * * @see https://dataclient.io/rest/api/Entity#merge */ static merge(existing, incoming) { return { ...existing, ...incoming }; } /** Run when an existing entity is found in the store * * @see https://dataclient.io/rest/api/Entity#mergeWithStore */ static mergeWithStore(existingMeta, incomingMeta, existing, incoming) { const shouldUpdate = this.shouldUpdate(existingMeta, incomingMeta, existing, incoming); if (shouldUpdate) { // distinct types are not mergeable (like delete symbol), so just replace if (typeof incoming !== typeof existing) { return incoming; } else { return this.shouldReorder(existingMeta, incomingMeta, existing, incoming) ? this.merge(incoming, existing) : this.merge(existing, incoming); } } else { return existing; } } /** Run when an existing entity is found in the store * * @see https://dataclient.io/rest/api/Entity#mergeMetaWithStore */ static mergeMetaWithStore(existingMeta, incomingMeta, existing, incoming) { return this.shouldReorder(existingMeta, incomingMeta, existing, incoming) ? existingMeta : incomingMeta; } /** Factory method to convert from Plain JS Objects. * * @param [props] Plain Object of properties to assign. */ static fromJS( // TODO: this should only accept members that are not functions props = {}) { // we type guarded abstract case above, so ok to force typescript to allow constructor call const instance = new this(props); // we can't rely on constructors and override the defaults provided as property assignments // all occur after the constructor Object.assign(instance, props); return instance; } /** Called when denormalizing an entity to create an instance when 'valid' * * @see https://dataclient.io/rest/api/Entity#createIfValid * @param [props] Plain Object of properties to assign. */ static createIfValid( // TODO: this should only accept members that are not functions props) { if (this.validate(props)) { return undefined; } return this.fromJS(props); } /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#process */ static process(input, parent, key, args) { return { ...input }; } static normalize(input, parent, key, args, visit, addEntity, getEntity, checkLoop) { const processedEntity = this.process(input, parent, key, args); let id; if (typeof processedEntity === 'undefined') { id = this.pk(input, parent, key, args); addEntity(this, INVALID, id); return id; } id = this.pk(processedEntity, parent, key, args); if (id === undefined || id === '' || id === 'undefined') { // create a random id if a valid one cannot be computed // this is useful for optimistic creates that don't need real ids - just something to hold their place id = `MISS-${Math.random()}`; // 'creates' conceptually should allow missing PK to make optimistic creates easy if (process.env.NODE_ENV !== 'production' && !visit.creating) { let why; if (!('pk' in options) && EntityMixin.prototype.pk === this.prototype.pk && !('id' in processedEntity)) { why = `'id' missing but needed for default pk(). Try defining pk() for your Entity.`; } else { why = `This is likely due to a malformed response. Try inspecting the network response or fetch() return value. Or use debugging tools: https://dataclient.io/docs/getting-started/debugging`; } const error = new Error(`Missing usable primary key when normalizing response. ${why} Learn more about primary keys: https://dataclient.io/rest/api/Entity#pk Entity: ${this.key} Value (processed): ${processedEntity && JSON.stringify(processedEntity, null, 2)} `); error.status = 400; throw error; } } else { id = `${id}`; } /* Circular reference short-circuiter */ if (checkLoop(this.key, id, input)) return id; const errorMessage = this.validate(processedEntity); throwValidationError(errorMessage); Object.keys(this.schema).forEach(key => { if (Object.hasOwn(processedEntity, key)) { processedEntity[key] = visit(this.schema[key], processedEntity[key], processedEntity, key, args); } }); addEntity(this, processedEntity, id); return id; } static validate(processedEntity) { return; } static queryKey(args, queryKey, getEntity, getIndex) { if (!args[0]) return; const id = queryKeyCandidate(this, args, getIndex); // ensure this actually has entity or we shouldn't try to use it in our query if (getEntity(this.key, id)) return id; } static denormalize(input, args, unvisit) { if (typeof input === 'symbol') { return input; } // note: iteration order must be stable for (const key of Object.keys(this.schema)) { const schema = this.schema[key]; const value = unvisit(schema, input[key]); if (typeof value === 'symbol') { // if default is not 'falsy', then this is required, so propagate INVALID symbol if (this.defaults[key]) { return value; } input[key] = undefined; } else { input[key] = value; } } return input; } /** All instance defaults set */ static get defaults() { // we use hasOwn because we don't want to use a parents' defaults if (!Object.hasOwn(this, '__defaults')) Object.defineProperty(this, '__defaults', { value: new this(), writable: true, configurable: true }); return this.__defaults; } } const { pk, schema, key, ...staticProps } = options; // remaining options Object.assign(EntityMixin, staticProps); if ('schema' in options) { EntityMixin.schema = options.schema; } else if (!Base.schema) { EntityMixin.schema = {}; } if ('pk' in options) { if (typeof options.pk === 'function') { EntityMixin.prototype.pk = function (parent, key) { return options.pk(this, parent, key); }; } else { EntityMixin.prototype.pk = function () { return this[options.pk]; }; } // default to 'id' field if the base class doesn't have a pk } else if (typeof Base.prototype.pk !== 'function') { EntityMixin.prototype.pk = function () { return this.id; }; } if ('key' in options) { Object.defineProperty(EntityMixin, 'key', { value: options.key, configurable: true, writable: true, enumerable: true }); } else if (!('key' in Base)) { function set(value) { Object.defineProperty(this, 'key', { value, writable: true, enumerable: true, configurable: true }); } const baseGet = function () { const name = this.name === 'EntityMixin' ? Base.name : this.name; /* istanbul ignore next */ if (process.env.NODE_ENV !== 'production' && (name === '' || name === 'EntityMixin' || name === '_temp')) throw new Error('Entity classes without a name must define `static key`\nSee: https://dataclient.io/rest/api/Entity#key'); return name; }; const get = /* istanbul ignore if */ typeof document !== 'undefined' && document.CLS_MANGLE ? /* istanbul ignore next */function () { document.CLS_MANGLE == null || document.CLS_MANGLE(this); Object.defineProperty(EntityMixin, 'key', { get: baseGet, set, enumerable: true, configurable: true }); return baseGet.call(this); } : baseGet; Object.defineProperty(EntityMixin, 'key', { get, set, enumerable: true, configurable: true }); } return EntityMixin; } function indexFromParams(params, indexes) { if (!indexes) return undefined; return indexes.find(index => Object.hasOwn(params, index)); } // part of the reason for pulling this out is that all functions that throw are deoptimized function throwValidationError(errorMessage) { if (errorMessage) { const error = new Error(errorMessage); error.status = 400; throw error; } } function queryKeyCandidate(schema, args, getIndex) { if (['string', 'number'].includes(typeof args[0])) { return `${args[0]}`; } const id = schema.pk(args[0], undefined, '', args); // Was able to infer the entity's primary key from params if (id !== undefined && id !== '') return id; // now attempt lookup in indexes const indexName = indexFromParams(args[0], schema.indexes); if (!indexName) return; const value = args[0][indexName]; return getIndex(schema.key, indexName, value)[value]; } /** * Programmatic cache reading * * @see https://dataclient.io/rest/api/Query */ class Query { /** * Programmatic cache reading * * @see https://dataclient.io/rest/api/Query */ constructor(schema, process) { this.schema = schema; this.process = process; } normalize(...args) { return this.schema.normalize(...args); } denormalize(input, args, unvisit) { const value = unvisit(this.schema, input); return typeof value === 'symbol' ? value : this.process(value, ...args); } queryKey(args, queryKey, getEntity, getIndex) { return queryKey(this.schema, args, getEntity, getIndex); } } /* istanbul ignore file */ var schema = /*#__PURE__*/Object.freeze({ __proto__: null, All: AllSchema, Array: ArraySchema, Collection: CollectionSchema, Entity: EntityMixin, EntityMixin: EntityMixin, Invalidate: Invalidate, Object: ObjectSchema, Query: Query, Union: UnionSchema, Values: ValuesSchema }); const EmptyBase = class {}; /** * Entity defines a single (globally) unique object. * @see https://dataclient.io/rest/api/Entity */ class Entity extends EntityMixin(EmptyBase) { /** Control how automatic schema validation is handled * * `undefined`: Defaults - throw error in worst offense * 'warn': only ever warn * 'silent': Don't bother with processing at all * * Note: this only applies to non-nested members. */ /** Factory method to convert from Plain JS Objects. * * @see https://dataclient.io/rest/api/Entity#fromJS * @param [props] Plain Object of properties to assign. */ /** * A unique identifier for each Entity * * @see https://dataclient.io/rest/api/Entity#pk * @param [value] POJO of the entity or subset used * @param [parent] When normalizing, the object which included the entity * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#process */ static process(input, parent, key, args) { /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && this.automaticValidation !== 'silent') { if (Array.isArray(input)) { const errorMessage = `Attempted to initialize ${this.name} with an array, but named members were expected This is likely due to a malformed response. Try inspecting the network response or fetch() return value. Or use debugging tools: https://dataclient.io/docs/getting-started/debugging Learn more about schemas: https://dataclient.io/docs/api/schema If this is a mistake, you can disable this check by setting static automaticValidation = 'silent' First three members: ${JSON.stringify(input.slice(0, 3), null, 2)}`; if (this.automaticValidation !== 'warn') { const error = new Error(errorMessage); error.status = 400; throw error; } console.warn(errorMessage); } } return super.process(input, parent, key, args); } } function validateRequired(processedEntity, requiredDefaults) { let missingKey = ''; if (Object.keys(requiredDefaults).some(key => { if (!Object.hasOwn(processedEntity, key)) { missingKey = key; return true; } return false; })) { return `Missing key ${missingKey}`; } } var _document$querySelect; /* istanbul ignore file */ const isBrowser = typeof document !== 'undefined'; let CSP = isBrowser && !((_document$querySelect = document.querySelector("meta[http-equiv='Content-Security-Policy']")) != null && _document$querySelect.getAttribute('content')); try { if (!CSP) Function(); } catch (e) { CSP = true; // TODO: figure out how to supress the error log instead of tell people it's okay if (isBrowser) { console.error('Content Security Policy: The previous CSP log can be safely ignored - @data-client/endpoint will use setPrototypeOf instead'); } } var _Endpoint; /** * Defines an async data source. * @see https://dataclient.io/docs/api/Endpoint */ class Endpoint extends Function { constructor(fetchFunction, options) { let self; if (CSP) { self = (...args) => self.fetch(...args); Object.setPrototypeOf(self, new.target.prototype); } else { super('return arguments.callee.fetch.apply(arguments.callee, arguments)'); self = this; } if (fetchFunction) self.fetch = fetchFunction; /** Name propery block * * To make things callable, we force every instance to be constructed as a function * Because of this the name property will be autoset * To create a usable naming inheritance pattern, we use __name as a proxy. * Every instance then overrides the name property. * * For protocol specific extensions that wish to customize default naming * behavior, be sure to add your own `Object.defineProperty(self, 'name'` * in your constructor to override this one. */ let autoName; if (!(options && 'name' in options) && fetchFunction && fetchFunction.name && fetchFunction.name !== 'anonymous') { autoName = fetchFunction.name; } Object.defineProperty(self, 'name', { get() { if (/* istanbul ignore else */process.env.NODE_ENV !== 'production' && self.key === Endpoint.prototype.key && !(autoName || this.__name)) { console.error('Endpoint: Autonaming failure.\n\nEndpoint initialized with anonymous function.\nPlease add `name` option or hoist the function definition. https://dataclient.io/rest/api/Endpoint#name'); } return autoName || this.__name; }, set(v) { this.__name = v; } }); /** End name property block */ Object.assign(self, options); return self; } key(...args) { return `${this.name} ${JSON.stringify(args)}`; } testKey(key) { return key.startsWith(this.name); } bind(thisArg, ...args) { const fetchFunc = this.fetch; const keyFunc = this.key; return this.extend({ fetch() { return fetchFunc.apply(thisArg != null ? thisArg : this, args); }, key() { return keyFunc.apply(this, args); } }); } extend(options) { // make a constructor/prototype based off this // extend from it and init with options sent class E extends this.constructor {} Object.assign(E.prototype, this); return new E(options.fetch, options); } /* istanbul ignore next */ } _Endpoint = Endpoint; (() => { /* istanbul ignore if */ if (typeof document !== 'undefined' && document.FUNC_MANGLE) { const baseKey = _Endpoint.prototype.key; _Endpoint.prototype.key = function (...args) { document.FUNC_MANGLE == null || document.FUNC_MANGLE(this); this.prototype.key = baseKey; return baseKey.call(this, ...args); }; } })(); const ExtendableEndpoint = Endpoint; Object.hasOwn = Object.hasOwn || /* istanbul ignore next */function hasOwn(it, key) { return Object.prototype.hasOwnProperty.call(it, key); }; exports.Endpoint = Endpoint; exports.Entity = Entity; exports.EntityMixin = EntityMixin; exports.ExtendableEndpoint = ExtendableEndpoint; exports.INVALID = INVALID; exports.schema = schema; exports.validateRequired = validateRequired;