import { Spec } from './jsonapiSpec' ; import { SchemaError } from './schemaChecker' ; import { memoized } from './memoized.decorator' ; export { Spec } ; /**
 * This is the main module of Grivet.
 *
 * It contains the [[Document]], [[Resource]] and [[Relationship]] classes that
 * perform most of the work interpreting a JSON:API structure.
 *
 * Normally you would start by creating a [[Document]] from existing JSON:API data or using the [[Document.fromURL]]
 * method to fetch data from a server. Then you can examine the resources in that document ([[Document.resource]]
 * or [[Document.resources]]) and traverse to other related resources ([[Resource.relatedResource]] or
 * [[Resource.relatedResources]]).
 *
 * You have to provide an implementation of the [[Context]] interface to specify how documents should be fetched from
 * URLs. The documentation of the [[Context]] interface shows several possible implementations (e.g. using the Angular http client
 * or Axios).
 *
 * Based on version 1.0 of JSON:API.
 */ export namespace JsonApi { /** Thrown when there is mismatch between the expected resource count (one or many) and the actual resource count */ export class CardinalityError extends Error { constructor ( message ? ) { super ( message ) ; Object . setPrototypeOf ( this , new . target . prototype ) ; } } /** Thrown when an explicitly provided id does not match the id received from the server */ export class IdMismatchError extends Error { constructor ( message ? ) { super ( message ) ; Object . setPrototypeOf ( this , new . target . prototype ) ; } } /**
     * Implement this interface to define how a [[JsonApiDocument]] (the JSON:API raw data) is fetched from a given URL.
     *
     * [[include:guides/context.md]]
     */ export interface Context { /**
         * Should return a `Promise` resolving to the JSON:API document for the given `url`
         */ getDocument ( url ) : Promise < Spec . JsonApiDocument > ; } /** List of fields that should be fetched from the server */ export type FieldNames = string [ ] ; /** Mapping from resource type to sparse fields */ export type SparseFields = { [ resourceType ] : FieldNames ; } ; see https export abstract class Resource { param document param id param type param context constructor ( protected readonly document , public readonly id , public readonly type , private readonly context ) { } protected abstract getData ( ) ; memoized @ memoized ( ) get rawData ( ) : Spec . ResourceObject { return this . getData ( ) ; } /**
         * Object containing all [attributes](https://jsonapi.org/format/1.0/#document-resource-object-attributes) of this resource
         */ get attributes ( ) : Spec . AttributesObject | undefined { return this . rawData . attributes ; } see https memoized @ memoized ( ) get relationships ( ) : Relationships { if ( ! this . rawData . relationships ) { return { } ; } const result = { } ; for ( const relationshipName in this . rawData . relationships ) { result [ relationshipName ] = new Relationship ( this . document , this . rawData . relationships [ relationshipName ] , this . context ) ; } return result ; } see https memoized @ memoized ( ) get links ( ) : Links { if ( ! this . rawData . links ) { return { } ; } const result = { } ; for ( const linkName in this . rawData . links ) { result [ linkName ] = new Link ( this . rawData . links [ linkName ] , this . document . url ) ; } return result ; } memoized @ memoized ( ) get metaLinks ( ) : Links { if ( ! this . rawData . meta ) { return { } ; } if ( ! this . rawData . meta . links ) { return { } ; } const result = { } ; for ( const linkName in this . rawData . meta . links ) { result [ linkName ] = new Link ( this . rawData . meta . links [ linkName ] , this . document . url ) ; } return result ; } memoized @ memoized ( ) get meta ( ) : Spec . MetaObject | undefined { return this . rawData . meta ; } /**
         * Self-link defined in the `links` member
         */ get selfLink ( ) : Link | undefined { return this . links [ 'self' ] ; } /**
         * Proxy providing all multiple [[Resource]]s reachable via relationships from this resource.
         * For example, for this JSON:API document
         *
         * ```json
         * {
         *   "data": {
         *     "type": "articles",
         *     "id": "1",
         *     "relationships": {
         *       "comments": {
         *         "data": [{"type":"comments", "id":"1"}, {"type":"comments", "id":"2"}]
         *       },
         *       "ratings": {
         *         "data": [{"type":"ratings", "id":"1"}, {"type":"ratings", "id":"2"}]
         *       }
         *     }
         *   },
         *   "included": [...]
         * }
         * ```
         *
         * calling `relatedResources` on the primary _articles_ resource will give you access to the following data:
         *
         * ```typescript
         * {
         *   comments: [
         *     RelatedResource(...),
         *     RelatedResource(...)
         *   ],
         *   ratings: [
         *     RelatedResource(...),
         *     RelatedResource(...)
         *   ]
         * }
         * ```
         *
         * This accessor is a shortcut so that you do not have to go through `relationships` manually every time.
         * Writing
         *
         * ```typescript
         * const comments = await article.relatedResources['comments'];
         * ```
         *
         * is equivalent to
         *
         * ```typescript
         * const comments = await article.relationships['comments'].resources();
         * ```
         *
         * As this shortcut uses the [[Relationship.resources]] method, it prefers resource linkage (i.e. id/type pairs in `data`)
         * to `related` links should both be present in the relationship. You can use the [[Relationship.resourcesFromRelatedLink]]
         * method if you need to fetch related resources via link instead of resource linkage.
         *
         * As this is only a proxy object, calling `relatedResources` will not yet construct any [[RelatedResource]]
         * instances. Only when accessing a specific related resource (e.g. calling `article.relatedResources['comments']`)
         * are the new resources actually constructed.
         */ get relatedResources ( ) { return new Proxy ( < RelationshipToResources > { } , new RelatedResourcesAccessor ( this ) ) ; } /**
         * Proxy providing all singular [[Resource]]s reachable via relationships from this resource.
         * For example, for this JSON:API document
         *
         * ```json
         * {
         *   "data": {
         *     "type": "articles",
         *     "id": "1",
         *     "relationships": {
         *       "author": {
         *         "data": {"type":"people", "id":"1"}
         *       },
         *       "reviewer": {
         *         "data": {"type":"people", "id":"12"}
         *       }
         *     }
         *   },
         *   "included": [...]
         * }
         * ```
         *
         * calling `relatedResource` on the primary _articles_ resource will give you access to the following data:
         *
         * ```typescript
         * {
         *   author: RelatedResource(...),
         *   reviewer: RelatedResource(...)
         * }
         * ```
         *
         * This accessor is a shortcut so that you do not have to go through `relationships` manually every time.
         * Writing
         *
         * ```typescript
         * const author = await article.relatedResource['author'];
         * ```
         *
         * is equivalent to
         *
         * ```typescript
         * const author = await article.relationships['author'].resource();
         * ```
         *
         * As this shortcut uses the [[Relationship.resource]] method, it prefers resource linkage (i.e. id/type pairs in `data`)
         * to `related` links should both be present in the relationship. You can use the [[Relationship.resourceFromRelatedLink]]
         * method if you need to fetch a related resource via link instead of resource linkage.
         *
         * As this is only a proxy object, calling `relatedResource` will not yet construct any [[RelatedResource]]
         * instances. Only when accessing a specific related resource (e.g. calling `article.relatedResource['author']`)
         * is the new resource actually constructed.
         */ get relatedResource ( ) { return new Proxy ( < RelationshipToResource > { } , new RelatedResourceAccessor ( this ) ) ; } /**
         * Proxy providing all [[Document]]s reachable via `related` links in relationships.
         * For example, for this JSON:API document
         *
         * ```json
         * {
         *   "data": {
         *     "type": "articles",
         *     "id": "1",
         *     "relationships": {
         *       "author": {
         *         "links": {"related": "http://example.com/people/1"}
         *       },
         *       "reviewer": {
         *         "links": {"related": "http://example.com/people/12"}
         *       }
         *     }
         *   }
         * }
         * ```
         *
         * calling `relatedDocuments` on the primary _articles_ resource will give you access to the following data:
         *
         * ```typescript
         * {
         *   author: Document(...),
         *   reviewer: Document(...)
         * }
         * ```
         *
         * As this is only a proxy object, calling `relatedDocuments` does not yet fetch any related documents.
         * Only when accessing a specific related document (e.g. calling `article.relatedDocuments['author']`) is the
         * new document actually requested from the server.
         */ get relatedDocuments ( ) { return new Proxy ( < RelationshipToDocument > { } , new RelatedDocumentAccessor ( this ) ) ; } } /**
     * A resource contained in the top level `data` member of a [[Document]].
     *
     * Always constructed non-lazily from raw JSON:API data. Normally this is
     * done automatically when accessing the [[Document.resource]] or [[Document.resources]]
     * accessors, for example:
     *
     * ```typescript
     * // a document containing one _article_ resource
     * const doc = await JsonApi.Document.fromURL(new URL('http://example.com/article'), context);
     * const article = doc.resource;  // the primary resource
     * ```
     *
     * Methods and accessors marked as `memoized` are only executed once per instance (the first time they are called)
     * and return a cached result on subsequent calls.
     */ export class PrimaryResource extends Resource { private readonly pRawData : Spec . ResourceObject ; param rawData param document param resourceType param context param id throws throws constructor ( rawData , document , resourceType , context , id ? ) { Spec . checkResourceObjectSchema ( rawData ) ; const passedId = id ; id = rawData . id ; if ( passedId !== undefined && id !== passedId ) { throw new IdMismatchError ( `ID in rawData does not match given ID: ${ id } != ${ passedId }` ) ; } super ( document , id , resourceType , context ) ; this . pRawData = rawData ; } protected getData ( ) { return this . pRawData ; } } see https export class Document { param context param sparseFields static async fromURL ( url , context , sparseFields ? ) { for ( const resourceType in sparseFields ) { url . searchParams . append ( `fields[${ resourceType }]` , sparseFields [ resourceType ] . join ( ',' ) ) ; } const rawData = await context . getDocument ( url ) ; return new Document ( rawData , context , url , sparseFields ) ; } param rawData param context param url param sparseFields throws constructor ( readonly rawData , private readonly context , public readonly url ? , public readonly sparseFields ? ) { Spec . checkDocumentSchema ( rawData ) ; } memoized @ memoized ( ) get hasManyResources ( ) : boolean { return Array . isArray ( this . rawData . data ) ; } returns throws memoized @ memoized ( ) get resources ( ) : PrimaryResource [ ] { if ( ! this . hasManyResources ) { throw new CardinalityError ( 'Document does not contain an array of resources. Use the `resource` property instead' ) ; } if ( ! ( 'data' in this . rawData ) ) { return [ ] ; } return ( < Spec . ResourceObject [ ] > this . rawData . data ) . map ( primaryData => new PrimaryResource ( primaryData , this , primaryData . type , this . context ) ) ; } returns throws memoized @ memoized ( ) get resource ( ) : PrimaryResource | null | undefined { if ( this . hasManyResources ) { throw new CardinalityError ( 'Document contains an array of resources. Use the `resources` property instead' ) ; } if ( this . rawData . data === null ) { return null ; } if ( ! ( 'data' in this . rawData ) ) { return undefined ; } const primaryData = < Spec . ResourceObject > this . rawData . data ; return new PrimaryResource ( primaryData , this , primaryData . type , this . context ) ; } memoized @ memoized ( ) get includedResources ( ) : IncludedResourcesMap { const res = { } ; for ( const includedResource of this . rawData . included || [ ] ) { if ( ! ( includedResource . type in res ) ) { res [ includedResource . type ] = { } ; } res [ includedResource . type ] [ includedResource . id ] = new RelatedResource ( this , includedResource . id , includedResource . type , this . context ) ; } return res ; } } /** Collection of [[RelatedResource]]s included in a compound document, organized by type and id */ export type IncludedResourcesMap = { [ type ] : { [ id ] : RelatedResource ; } ; } ; /** Mapping from relationship name to [[Relationship]] */ export type Relationships = { [ relationshipName ] : Relationship ; } ; /** Mapping from link name to [[Link]] */ export type Links = { [ linkName ] : Link ; } ; hidden type RelationshipToResource = { [ relationshipName ] : Promise < Resource > ; } ; hidden type RelationshipToResources = { [ relationshipName ] : Promise < Resource [ ] > ; } ; hidden type RelationshipToDocument = { [ relationshipName ] : Promise < Document > ; } ; hidden class RelatedResourceAccessor < T extends RelationshipToResource > implements ProxyHandler < T > { constructor ( private readonly parent ) { } hidden async get ( target , relationshipName , receiver ) { if ( relationshipName in this . parent . relationships ) { return this . parent . relationships [ relationshipName ] . resource ( ) ; } } } hidden class RelatedResourcesAccessor < T extends RelationshipToResources > implements ProxyHandler < T > { constructor ( private readonly parent ) { } hidden async get ( target , relationshipName , receiver ) { if ( relationshipName in this . parent . relationships ) { return this . parent . relationships [ relationshipName ] . resources ( ) ; } return [ ] ; } } hidden class RelatedDocumentAccessor < T extends RelationshipToDocument > implements ProxyHandler < T > { constructor ( private readonly parent ) { } hidden async get ( target , relationshipName , receiver ) { if ( relationshipName in this . parent . relationships ) { return this . parent . relationships [ relationshipName ] . relatedDocument ( ) ; } } } /**
     * A resource contained in the top level `included` member of a [[Document]] or linked via href.
     *
     * This resource is initialized lazily on demand when its [[getData]] method is first called.
     * The resource then looks for its id and type (given at construction) in the parent document
     * `data` and `included` members to find its raw data.
     *
     * Normally a related resource is obtained via the [[Resource.relatedResource]] and [[Resource.relatedResources]]
     * accessors or the [[Relationship.resource]] and [[Relationship.resources]] methods, for example:
     *
     * ```typescript
     * // a document containing one _article_ resource with a related _author_ resource
     * const doc = await JsonApi.Document.fromURL(new URL('http://example.com/article'), context);
     * const article = doc.resource;  // the primary resource
     * const author = await article.relatedResource['author'];  // the related resource
     * ```
     *
     * Methods and accessors marked as `memoized` are only executed once per instance (the first time they are called)
     * and return a cached result on subsequent calls.
     */ export class RelatedResource extends Resource { param document param id param resourceType param context throws constructor ( document , id , resourceType , context ) { super ( document , id , resourceType , context ) ; } throws throws protected getData ( ) { const primaryDataArray = this . document . hasManyResources ? < Spec . ResourceObject [ ] > this . document . rawData . data : [ < Spec . ResourceObject > this . document . rawData . data ] ; const candidates = primaryDataArray . concat ( this . document . rawData . included || [ ] ) ; const filtered = candidates . filter ( resourceObject => resourceObject . type === this . type && resourceObject . id === this . id ) ; if ( filtered . length === 0 ) { throw new IdMismatchError ( `Resource with id "${ this . id }" and type "${ this . type }" not found in document` ) ; } if ( filtered . length > 1 ) { throw new IdMismatchError ( `Resource with id "${ this . id }" and type "${ this . type }" found more than once in document` ) ; } Spec . checkResourceObjectSchema ( filtered [ 0 ] ) ; return filtered [ 0 ] ; } } see https export class Link { /** The complete url for this link */ readonly url : URL ; /** Any additional meta data */ readonly meta ? : object ; param rawData param referringDocumentURL constructor ( rawData , referringDocumentURL ? ) { const origin = referringDocumentURL ? referringDocumentURL . origin : '' ; if ( typeof rawData === 'string' ) { try { this . url = new URL ( rawData ) ; } catch ( e ) { this . url = new URL ( origin + rawData ) ; } } else { try { this . url = new URL ( rawData . href ) ; } catch ( e ) { this . url = new URL ( origin + rawData . href ) ; } this . meta = rawData . meta ; } } } see https export class Relationship { param referringDocument param rawData param context throws constructor ( private readonly referringDocument , private readonly rawData , private readonly context ) { Spec . checkRelationshipObjectSchema ( rawData ) ; } memoized @ memoized ( ) get empty ( ) : boolean { return this . links === undefined && this . data === undefined ; } memoized @ memoized ( ) get links ( ) : Links | undefined { if ( ! this . rawData . links ) { return undefined ; } const result = { } ; for ( const linkName in this . rawData . links ) { result [ linkName ] = new Link ( this . rawData . links [ linkName ] , this . referringDocument . url ) ; } return result ; } memoized @ memoized ( ) get data ( ) : Spec . ResourceIdentifierObject | Spec . ResourceIdentifierObject [ ] | undefined | null { return this . rawData . data ; } memoized @ memoized ( ) async relatedDocument ( ) { if ( this . links && 'related' in this . links ) { return Document . fromURL ( this . links [ 'related' ] . url , this . context , this . referringDocument . sparseFields ) ; } } throws throws memoized @ memoized ( ) async resources ( ) { if ( 'data' in this . rawData ) { const resourceIdentifiers = this . data ; if ( resourceIdentifiers !== null && resourceIdentifiers !== undefined ) { if ( ! Array . isArray ( resourceIdentifiers ) ) { throw new CardinalityError ( 'Relationship does not contain an array of resources. Use the `resource` method instead.' ) ; } return Promise . resolve ( resourceIdentifiers . map ( rid => new RelatedResource ( this . referringDocument , rid . id , rid . type , this . context ) ) ) ; } } const relatedDoc = await this . relatedDocument ( ) ; if ( relatedDoc ) { return relatedDoc . resources ; } throw new SchemaError ( 'A relationship object relating to a resource must contain a `links.related` or `data` member' ) ; } throws throws memoized @ memoized ( ) async resource ( ) { if ( 'data' in this . rawData ) { const resourceIdentifier = this . data ; if ( resourceIdentifier !== null && resourceIdentifier !== undefined ) { if ( Array . isArray ( resourceIdentifier ) ) { throw new CardinalityError ( 'Relationship contains more than one resource. Use the `resources` method instead.' ) ; } return Promise . resolve ( new RelatedResource ( this . referringDocument , resourceIdentifier . id , resourceIdentifier . type , this . context ) ) ; } } const relatedDoc = await this . relatedDocument ( ) ; if ( relatedDoc ) { return relatedDoc . resource ; } if ( 'data' in this . rawData ) { const resourceIdentifier = this . data ; if ( resourceIdentifier === null ) { return null ; } if ( resourceIdentifier === undefined ) { return undefined ; } } throw new SchemaError ( 'A relationship object relating to a resource must contain a `links.related` or `data` member' ) ; } throws throws memoized @ memoized ( ) async resourcesFromRelatedLink ( ) { try { const relatedDoc = await this . relatedDocument ( ) ; if ( relatedDoc ) { return relatedDoc . resources ; } } catch ( err ) { } return this . resources ( ) ; } throws throws memoized @ memoized ( ) async resourceFromRelatedLink ( ) { try { const relatedDoc = await this . relatedDocument ( ) ; if ( relatedDoc ) { return relatedDoc . resource ; } } catch ( err ) { } return this . resource ( ) ; } } hidden export class ClientDocument { private readonly rawData : Spec . ClientJsonApiDocument ; constructor ( resourceType , id ? ) { this . rawData = { data : { type : resourceType } } ; if ( id !== undefined ) { this . rawData . data . id = id ; } } /** Sets a primary resource attribute @hidden */ setAttribute ( name , value ) { if ( ! this . rawData . data . attributes ) { this . rawData . data . attributes = { } ; } this . rawData . data . attributes [ name ] = value ; } /** Adds a named relationship to a resource @hidden */ setRelationship ( name , ressourceIdentifier ) { if ( ! this . rawData . data . relationships ) { this . rawData . data . relationships = { } ; } this . rawData . data . relationships [ name ] = { data : ressourceIdentifier } ; } /** Adds the resource to `included` @hidden */ includeResource ( resource ) { if ( ! this . rawData . included ) { this . rawData . included = [ ] ; } this . rawData . included . push ( resource ) ; } /** Adds the resources to `included` @hidden */ includeResources ( resources ) { if ( ! this . rawData . included ) { this . rawData . included = [ ] ; } this . rawData . included . push ( ... resources ) ; } /** The raw JSON:API data @hidden */ get data ( ) : Spec . ClientJsonApiDocument { return this . rawData ; } } }