UNPKG

rest-client-sdk

Version:
1 lines 139 kB
{"version":3,"file":"index.cjs","sources":["../src/ErrorFactory.ts","../src/Mapping.ts","../src/Mapping/Attribute.ts","../src/utils/repositoryGenerator.ts","../src/client/headerUtils.ts","../src/client/AbstractClient.ts","../src/Mapping/ClassMetadata.ts","../src/Mapping/Relation.ts","../src/isImmutable.ts","../src/UnitOfWork.ts","../src/serializer/Serializer.ts","../src/serializer/JsSerializer.ts","../src/utils/levenshtein.ts","../src/utils/logging.ts","../src/RestClientSdk.ts","../src/TokenGenerator/AbstractTokenGenerator.ts","../src/TokenGenerator/AuthorizationCodeFlowTokenGenerator.ts","../src/decorator.ts","../src/TokenGenerator/ClientCredentialsGenerator.ts","../src/TokenGenerator/PasswordGenerator.ts","../src/TokenGenerator/ProvidedTokenGenerator.ts","../src/TokenStorage.ts"],"sourcesContent":["/* eslint-disable max-classes-per-file, no-use-before-define */\n/**\n * It's a bit tricky to extends native errors\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error\n */\n\nimport { ErrorBody, Token, TokenBody } from './TokenGenerator/types';\n\nclass HttpError extends Error {\n public baseResponse: Response;\n\n constructor(message: null | string, baseResponse: Response) {\n super(message || 'Http errort');\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, OauthError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, OauthError);\n }\n\n this.name = 'HttpError';\n this.baseResponse = baseResponse;\n }\n\n /**\n * Get the JSON of the baseResponse directly, if possible\n */\n public responseJson(): Promise<Record<string, unknown>> {\n return this.baseResponse.json();\n }\n}\n\nclass OauthError extends Error {\n public previousError: HttpError | undefined;\n\n constructor(message: null | string, previousError?: HttpError) {\n super(message || 'Oauth error');\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, OauthError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, OauthError);\n }\n\n this.name = 'OauthError';\n this.previousError = previousError;\n }\n}\n\nclass InvalidGrantError extends OauthError {\n public previousError: HttpError | undefined;\n\n constructor(message: null | string, previousError?: HttpError) {\n super(message || 'Invalid grant error', previousError);\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, InvalidGrantError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, InvalidGrantError);\n }\n\n this.name = 'InvalidGrantError';\n this.previousError = previousError;\n }\n}\n\nclass InvalidScopeError extends OauthError {\n public previousError: HttpError | undefined;\n\n constructor(message: null | string, previousError?: HttpError) {\n super(message || 'Invalid scope error', previousError);\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, InvalidScopeError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, InvalidScopeError);\n }\n\n this.name = 'InvalidScopeError';\n this.previousError = previousError;\n }\n}\n\n// 400\nclass BadRequestError extends HttpError {\n public baseResponse: Response;\n\n constructor(message: null | string, baseResponse: Response) {\n super(message || 'Bad request', baseResponse);\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, BadRequestError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, BadRequestError);\n }\n\n this.name = 'BadRequestError';\n this.baseResponse = baseResponse;\n }\n}\n\n// 401\nclass UnauthorizedError extends BadRequestError {\n public baseResponse: Response;\n\n constructor(message: null | string, baseResponse: Response) {\n super(message || 'Unauthorized', baseResponse);\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, UnauthorizedError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, UnauthorizedError);\n }\n\n this.name = 'UnauthorizedError';\n this.baseResponse = baseResponse;\n }\n}\n\n// 403\nclass ForbiddenError extends BadRequestError {\n public baseResponse: Response;\n\n constructor(message: null | string, baseResponse: Response) {\n super(message || 'Forbidden', baseResponse);\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, ForbiddenError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, ForbiddenError);\n }\n\n this.name = 'ForbiddenError';\n this.baseResponse = baseResponse;\n }\n}\n\n// 404\nclass ResourceNotFoundError extends BadRequestError {\n public baseResponse: Response;\n\n constructor(message: null | string, baseResponse: Response) {\n super(message || 'Resource is not found', baseResponse);\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, ResourceNotFoundError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, ResourceNotFoundError);\n }\n\n this.name = 'ResourceNotFoundError';\n this.baseResponse = baseResponse;\n }\n}\n\n// 409\nclass ConflictError extends BadRequestError {\n public baseResponse: Response;\n\n constructor(message: null | string, baseResponse: Response) {\n super(message || 'Conflict detected', baseResponse);\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, ConflictError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, ConflictError);\n }\n\n this.name = 'ConflictError';\n this.baseResponse = baseResponse;\n }\n}\n\n// 500\nclass InternalServerError extends HttpError {\n public baseResponse: Response;\n\n constructor(message: null | string, baseResponse: Response) {\n super(message || 'Internal server error', baseResponse);\n\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, InternalServerError.prototype);\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, InternalServerError);\n }\n\n this.name = 'InternalServerError';\n this.baseResponse = baseResponse;\n }\n}\n\n/**\n * @returns {HttpError}\n */\nfunction getHttpErrorFromResponse(originalResponse: Response): HttpError {\n const response = originalResponse.clone();\n switch (true) {\n case response.status === 401:\n return new UnauthorizedError(null, response);\n\n case response.status === 403:\n return new ForbiddenError(null, response);\n\n case response.status === 404:\n return new ResourceNotFoundError(null, response);\n\n case response.status === 409:\n return new ConflictError(null, response);\n\n case response.status >= 400 && response.status < 500:\n return new BadRequestError(null, response);\n\n case response.status >= 500 && response.status < 600:\n return new InternalServerError(null, response);\n\n default:\n return new HttpError(null, response);\n }\n}\n\nfunction isOauthError(body: TokenBody<Token>): body is ErrorBody {\n return typeof (body as ErrorBody)?.error === 'string';\n}\n\nexport {\n UnauthorizedError,\n BadRequestError,\n ConflictError,\n ForbiddenError,\n HttpError,\n InternalServerError,\n ResourceNotFoundError,\n getHttpErrorFromResponse,\n OauthError,\n InvalidGrantError,\n InvalidScopeError,\n isOauthError,\n};\n","import ClassMetadata from './Mapping/ClassMetadata';\n\nconst DEFAULT_CONFIG = {\n collectionKey: 'hydra:member',\n};\n\nclass Mapping {\n readonly idPrefix: string;\n\n #config!: Record<string, unknown>;\n\n #classMetadataList: ClassMetadata[];\n\n constructor(idPrefix = '', config = {}) {\n this.idPrefix = idPrefix;\n\n this.#classMetadataList = [];\n\n this.setConfig(config);\n }\n\n getConfig(): Record<string, unknown> {\n return this.#config;\n }\n\n setConfig(config: Record<string, unknown>): void {\n this.#config = { ...DEFAULT_CONFIG, ...config };\n }\n\n setMapping(classMetadataList: ClassMetadata[] = []): void {\n this.#classMetadataList = classMetadataList;\n }\n\n getMappingKeys(): string[] {\n return this.#classMetadataList.map((classMetadata) => classMetadata.key);\n }\n\n getClassMetadataByKey(key: string): null | ClassMetadata {\n const filterList = this.#classMetadataList.filter(\n (classMetadata) => classMetadata.key === key\n );\n\n return filterList.length > 0 ? filterList[0] : null;\n }\n\n isMappingValid(): boolean {\n return !this.#classMetadataList.some((classMetadata) => {\n // check that the relations exists\n const errorList = Object.values(classMetadata.getAttributeList()).map(\n (attribute) => {\n const relation = classMetadata.getRelation(attribute.serializedKey);\n\n if (!relation) {\n // attribute can not be \"invalid\" (for now ?)\n return false;\n }\n\n const relationMetadata = this.getClassMetadataByKey(\n relation.targetMetadataKey\n );\n\n // relation name seems weird\n if (\n relation.isRelationToOne() &&\n attribute.attributeName.endsWith('List')\n ) {\n return `\"${classMetadata.key}.${attribute.serializedKey} is defined as a MANY_TO_ONE relation, but the attribute name ends with \"List\".`;\n }\n\n if (relation.isRelationToMany()) {\n const message = `\"${classMetadata.key}.${attribute.serializedKey} is defined as a ${relation.type} relation, but the attribute name is nor plural not ends with \"List\".`;\n\n const endsWithList = attribute.attributeName.endsWith('List');\n\n try {\n // eslint-disable-next-line global-require, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires\n const pluralize = require('pluralize');\n if (\n !endsWithList &&\n !pluralize.isPlural(attribute.attributeName)\n ) {\n return message;\n }\n } catch (e) {\n if (!endsWithList) {\n return `${message}.\\nIf your keys does not ends with \"List\", then you should install the \"pluralize\" package.`;\n }\n }\n }\n\n // no error if there is metadata linked\n if (!relationMetadata) {\n return `\"${classMetadata.key}.${attribute.serializedKey}\" defined a relation to the metadata named \"${relation.targetMetadataKey}\" but this metadata is not knowned by the mapping`;\n }\n\n return false;\n }\n );\n\n if (!classMetadata.hasIdentifierAttribute()) {\n errorList.push(\n `\"${classMetadata.key}\" has no identifier attribute set`\n );\n }\n\n const nbError = errorList.filter((error) => {\n if (error) {\n // eslint-disable-next-line no-console\n console.warn(error);\n }\n\n return error;\n });\n\n return nbError.length > 0;\n });\n }\n}\n\nexport default Mapping;\n","class Attribute {\n readonly serializedKey: string;\n\n readonly attributeName: string;\n\n readonly type: string;\n\n readonly isIdentifier: boolean;\n\n /**\n * @param {string} serializedKey the key returned from your API\n * @param {null|string} attributeName the name in your entity, default to the `serializedKey` attribute\n * @param {string} type type of the attribute\n * @param {boolean} isIdentifier is this attribute the entity identifier\n */\n constructor(\n serializedKey: string,\n attributeName: null | string = null,\n type = 'string',\n isIdentifier = false\n ) {\n if (!serializedKey) {\n throw new TypeError('`serializedKey` must not be empty');\n }\n\n this.serializedKey = serializedKey;\n this.attributeName = attributeName || this.serializedKey;\n this.type = type;\n this.isIdentifier = isIdentifier;\n }\n}\n\nexport default Attribute;\n","/* eslint-disable import/prefer-default-export */\nimport type ClassMetadata from '../Mapping/ClassMetadata';\n// eslint-disable-next-line import/no-duplicates\nimport type RestClientSdk from '../RestClientSdk';\n// eslint-disable-next-line import/no-duplicates\nimport type { MetadataDefinition, SdkMetadata } from '../RestClientSdk';\nimport type AbstractClient from '../client/AbstractClient';\n\nexport function generateRepository<D extends MetadataDefinition>(\n sdk: RestClientSdk<SdkMetadata>,\n metadata: ClassMetadata,\n isUnitOfWorkEnabled = true\n): AbstractClient<D> {\n // eslint-disable-next-line new-cap\n return new metadata.repositoryClass(sdk, metadata, isUnitOfWorkEnabled);\n}\n","/* eslint-disable consistent-return */\nexport function removeAuthorization(\n headers: undefined | HeadersInit\n): undefined | HeadersInit {\n if (!headers) {\n return;\n }\n\n if (headers instanceof Headers) {\n headers.delete('Authorization');\n return headers;\n }\n\n const filterAuthorization = (entries: string[][]): string[][] =>\n entries.filter((header) => header[0] !== 'Authorization');\n\n if (Array.isArray(headers)) {\n return filterAuthorization(headers);\n }\n\n return Object.fromEntries(filterAuthorization(Object.entries(headers)));\n}\n\n/**\n * remove undefined key, usefull to remove Content-Type for example.\n * Does not apply on \"Headers\" instance as values are string in there.\n */\nexport function removeUndefinedHeaders(headers: HeadersInit): HeadersInit {\n if (headers instanceof Headers) {\n return headers;\n }\n\n const filterEntries = (entries: string[][]): string[][] =>\n entries.filter((header) => header[1] !== undefined);\n\n if (Array.isArray(headers)) {\n return filterEntries(headers);\n }\n\n return Object.fromEntries(filterEntries(Object.entries(headers)));\n}\n\nexport function convertToRecord(headers: HeadersInit): Record<string, string> {\n if (headers instanceof Headers) {\n return Object.fromEntries(headers.entries());\n }\n\n if (Array.isArray(headers)) {\n return Object.fromEntries(headers);\n }\n\n return headers;\n}\n","import URI from 'urijs';\nimport { OauthError, getHttpErrorFromResponse } from '../ErrorFactory';\nimport type ClassMetadata from '../Mapping/ClassMetadata';\n// eslint-disable-next-line import/no-duplicates\nimport type RestClientSdk from '../RestClientSdk';\n// eslint-disable-next-line import/no-duplicates\nimport type { MetadataDefinition, SdkMetadata } from '../RestClientSdk';\nimport { Token } from '../TokenGenerator/types';\nimport TokenStorageInterface from '../TokenStorageInterface';\nimport type SerializerInterface from '../serializer/SerializerInterface';\nimport { generateRepository } from '../utils/repositoryGenerator';\nimport { removeAuthorization, removeUndefinedHeaders } from './headerUtils';\n\nconst EXPIRE_LIMIT_SECONDS = 300; // = 5 minutes\n\nclass AbstractClient<D extends MetadataDefinition> {\n sdk: RestClientSdk<SdkMetadata>;\n\n #tokenStorage: TokenStorageInterface<Token>;\n\n serializer: SerializerInterface;\n\n metadata: ClassMetadata;\n\n #isUnitOfWorkEnabled: boolean;\n\n constructor(\n sdk: RestClientSdk<SdkMetadata>,\n metadata: ClassMetadata,\n isUnitOfWorkEnabled = true\n ) {\n this.sdk = sdk;\n this.#tokenStorage = sdk.tokenStorage;\n this.serializer = sdk.serializer;\n this.metadata = metadata;\n this.#isUnitOfWorkEnabled = isUnitOfWorkEnabled;\n }\n\n withUnitOfWork(enabled: boolean): AbstractClient<D> {\n // eslint-disable-next-line new-cap\n return generateRepository<D>(this.sdk, this.metadata, enabled);\n }\n\n getDefaultParameters(): Record<string, unknown> {\n return {};\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n getPathBase(pathParameters: Record<string, unknown>): string {\n return `/${this.metadata.pathRoot}`;\n }\n\n getEntityURI(entity: D['entity']): string {\n let idValue = this._getEntityIdentifier(entity);\n\n if (idValue === null) {\n throw new Error('Cannot call `getEntityURI for entity without id.`');\n }\n\n if (typeof idValue === 'number') {\n if (Number.isFinite(idValue)) {\n idValue = idValue.toString();\n } else {\n throw new Error('Unable to handle non-finite number');\n }\n }\n\n const pathBase = this.getPathBase({});\n if (idValue.indexOf(pathBase) > -1) {\n return idValue;\n }\n return `${pathBase}/${idValue}`;\n }\n\n /**\n * get an entity by its id\n *\n * @param {string|number} id the entity identifier\n * @param {Record<string, unknown>} queryParam query parameters that will be added to the request\n * @param {Record<string, unknown>} pathParameters path parameters, will be pass to the `getPathBase` method\n * @param {Record<string, unknown>} requestParams parameters that will be send as second parameter to the `fetch` call\n */\n find(\n id: string | number,\n queryParam: Record<string, unknown> = {},\n pathParameters: Record<string, unknown> = {},\n requestParams: Record<string, unknown> = {}\n ): Promise<D['entity']> {\n const url = this._generateUrlFromParams(queryParam, pathParameters, id);\n\n return this.deserializeResponse(\n this.authorizedFetch(url, requestParams),\n 'item'\n );\n }\n\n /**\n * get a list of entities by some parameters\n *\n * @param {Record<string, unknown>} queryParam query parameters that will be added to the request\n * @param {Record<string, unknown>} pathParameters path parameters, will be pass to the `getPathBase` method\n * @param {Record<string, unknown>} requestParams parameters that will be send as second parameter to the `fetch` call\n */\n findBy(\n queryParam: Record<string, unknown>,\n pathParameters: Record<string, unknown> = {},\n requestParams: Record<string, unknown> = {}\n ): Promise<D['list']> {\n const url = this._generateUrlFromParams(queryParam, pathParameters);\n\n return this.deserializeResponse(\n this.authorizedFetch(url, requestParams),\n 'list'\n );\n }\n\n /**\n * get a list of all entities\n *\n * @param {Record<string, unknown>} queryParam query parameters that will be added to the request\n * @param {Record<string, unknown>} pathParameters path parameters, will be pass to the `getPathBase` method\n * @param {Record<string, unknown>} requestParams parameters that will be send as second parameter to the `fetch` call\n */\n findAll(\n queryParam: Record<string, unknown> = {},\n pathParameters: Record<string, unknown> = {},\n requestParams: Record<string, unknown> = {}\n ): Promise<D['list']> {\n return this.findBy(queryParam, pathParameters, requestParams);\n }\n\n /**\n * create an entity\n *\n * @param {Record<string, unknown>} entity the entity to persist\n * @param {Record<string, unknown>} queryParam query parameters that will be added to the request\n * @param {Record<string, unknown>} pathParameters path parameters, will be pass to the `getPathBase` method\n * @param {Record<string, unknown>} requestParams parameters that will be send as second parameter to the `fetch` call\n */\n create(\n entity: D['entity'],\n queryParam: Record<string, unknown> = {},\n pathParameters: Record<string, unknown> = {},\n requestParams: Record<string, unknown> = {}\n ): Promise<D['entity']> {\n const url = new URI(this.getPathBase(pathParameters));\n url.addSearch(queryParam);\n\n const oldSerializedModel = this.metadata.getDefaultSerializedModel();\n const newSerializedModel = this.serializer.normalizeItem(\n entity,\n this.metadata\n );\n\n const diff = this.sdk.unitOfWork.getDirtyData(\n newSerializedModel,\n oldSerializedModel,\n this.metadata\n );\n\n return this.deserializeResponse(\n this.authorizedFetch(url, {\n method: 'POST',\n body: this.serializer.encodeItem(diff, this.metadata),\n ...requestParams,\n }),\n 'item'\n );\n }\n\n /**\n * update an entity\n *\n * @param {Record<string, unknown>} entity the entity to update\n * @param {Record<string, unknown>} queryParam query parameters that will be added to the request\n * @param {Record<string, unknown>} requestParams parameters that will be send as second parameter to the `fetch` call\n */\n update(\n entity: D['entity'],\n queryParam: Record<string, unknown> = {},\n requestParams: Record<string, unknown> = {}\n ): Promise<D['entity']> {\n const url = new URI(this.getEntityURI(entity));\n url.addSearch(queryParam);\n\n let newSerializedModel = this.serializer.normalizeItem(\n entity,\n this.metadata\n );\n\n const identifier = this._getEntityIdentifier(newSerializedModel);\n let oldModel;\n if (identifier !== null) {\n oldModel = this.sdk.unitOfWork.getDirtyEntity(identifier);\n }\n\n if (oldModel) {\n newSerializedModel = this.sdk.unitOfWork.getDirtyData(\n newSerializedModel,\n oldModel,\n this.metadata\n );\n }\n\n return this.deserializeResponse(\n this.authorizedFetch(url, {\n method: 'PUT',\n body: this.serializer.encodeItem(newSerializedModel, this.metadata),\n ...requestParams,\n }),\n 'item'\n );\n }\n\n /**\n * delete an entity\n *\n * @param {Record<string, unknown>} the entity to delete\n * @param {Record<string, unknown>} requestParams parameters that will be send as second parameter to the `fetch` call\n */\n delete(entity: D['entity'], requestParams = {}): Promise<Response> {\n const url = this.getEntityURI(entity);\n const identifier = this._getEntityIdentifier(entity);\n\n return this.authorizedFetch(url, {\n method: 'DELETE',\n ...requestParams,\n }).then((response) => {\n if (identifier !== null) {\n this.sdk.unitOfWork.clear(identifier);\n }\n\n return response;\n });\n }\n\n deserializeResponse(\n requestPromise: Promise<Response>,\n listOrItem: 'list'\n ): Promise<D['list']>;\n\n deserializeResponse(\n requestPromise: Promise<Response>,\n listOrItem: 'item'\n ): Promise<D['entity']>;\n\n deserializeResponse<LOR extends 'list' | 'item'>(\n requestPromise: Promise<Response>,\n listOrItem: LOR\n ): Promise<D['entity'] | D['list']> {\n return requestPromise\n .then((response) => response.text().then((text) => ({ response, text })))\n .then(({ response, text }) => {\n if (listOrItem === 'list') {\n // for list, we need to deserialize the result to get an object\n const itemList = this.serializer.deserializeList(\n text,\n this.metadata,\n response\n ) as D['list'];\n\n if (this.#isUnitOfWorkEnabled) {\n // eslint-disable-next-line no-restricted-syntax\n for (const decodedItem of itemList) {\n const identifier = this._getEntityIdentifier(decodedItem);\n const normalizedItem = this.serializer.normalizeItem(\n decodedItem,\n this.metadata\n );\n\n // then we register the re-normalized item\n if (identifier !== null) {\n this.sdk.unitOfWork.registerClean(identifier, normalizedItem);\n }\n }\n }\n\n return itemList as D['list'];\n }\n\n // for items, we can just decode the item (ie. transform it to JS object)\n const decodedItem = this.serializer.decodeItem(\n text,\n this.metadata,\n response\n );\n\n // and register it directy without deserializing + renormalizing\n const identifier = this._getEntityIdentifier(decodedItem);\n\n // and finally return the denormalized item\n const item = this.serializer.denormalizeItem(\n decodedItem,\n this.metadata,\n response\n ) as D['entity'];\n\n if (this.#isUnitOfWorkEnabled && identifier !== null) {\n this.sdk.unitOfWork.registerClean(\n identifier,\n this.serializer.normalizeItem(item, this.metadata)\n );\n }\n\n return item as D['entity'];\n });\n }\n\n makeUri(input: string | URI): URI {\n const url = input instanceof URI ? input : new URI(input);\n url.host(this.sdk.config.path).scheme(this.sdk.config.scheme);\n\n if (this.sdk.config.port) {\n url.port(String(this.sdk.config.port));\n }\n\n if (this.sdk.mapping.idPrefix) {\n const segments = url.segment();\n\n if (!url.pathname().startsWith(this.sdk.mapping.idPrefix)) {\n segments.unshift(this.sdk.mapping.idPrefix);\n url.segment(segments);\n }\n }\n\n return url;\n }\n\n authorizedFetch(input: string | URI, requestParams = {}): Promise<Response> {\n const url = this.makeUri(input);\n\n return this._fetchWithToken(url.toString(), requestParams);\n }\n\n private _generateUrlFromParams(\n queryParam: Record<string, unknown>,\n pathParameters: Record<string, unknown> = {},\n id: null | string | number = null\n ): URI {\n const params = queryParam;\n if (this.sdk.config.useDefaultParameters) {\n Object.assign(params, this.getDefaultParameters());\n }\n\n const pathBase = this.getPathBase(pathParameters);\n\n let url = null;\n if (id) {\n const testPathBase = this.sdk.mapping.idPrefix\n ? `${this.sdk.mapping.idPrefix}${pathBase}`\n : pathBase;\n\n if (typeof id === 'string' && id.startsWith(testPathBase)) {\n url = new URI(id);\n } else {\n url = new URI(`${pathBase}/${id}`);\n }\n } else {\n url = new URI(pathBase);\n }\n\n if (params) {\n url.addSearch(params);\n }\n\n return url;\n }\n\n private _fetchWithToken(\n input: string,\n requestParams = {}\n ): Promise<Response> {\n if (!input) {\n throw new Error('input is empty');\n }\n\n if (this.#tokenStorage) {\n return this.#tokenStorage\n .getCurrentTokenExpiresIn()\n .then((accessTokenExpiresIn) => {\n if (\n accessTokenExpiresIn !== null &&\n accessTokenExpiresIn <= EXPIRE_LIMIT_SECONDS\n ) {\n return this.#tokenStorage\n .refreshToken()\n .then(\n (refreshedTokenObject) => refreshedTokenObject.access_token\n ).catch((e) => {\n this._handleRefreshTokenFailure(e);\n\n throw e;\n });\n }\n\n return this.#tokenStorage.getAccessToken();\n })\n .then((token) => this._doFetch(token, input, requestParams));\n }\n\n return this._doFetch(null, input, requestParams);\n }\n\n private _refreshTokenAndRefetch(\n input: string,\n requestParams: RequestInit = {}\n ): Promise<Response> {\n return this.#tokenStorage\n .refreshToken()\n .then(() => {\n // eslint-disable-next-line prefer-const\n let { headers, ...rest } = requestParams;\n\n const updatedRequestParams: RequestInit = {\n ...rest,\n headers: removeAuthorization(headers),\n };\n\n return this._fetchWithToken(input, updatedRequestParams);\n })\n .catch((e) => {\n this._handleRefreshTokenFailure(e);\n\n throw e;\n });\n }\n\n private _manageUnauthorized(\n response: Response,\n input: string,\n requestParams = {}\n ): Promise<Response> {\n const error = getHttpErrorFromResponse(response);\n\n // https://tools.ietf.org/html/rfc2617#section-1.2\n const authorizationHeader = response.headers.get('www-authenticate');\n if (authorizationHeader) {\n const invalidGrant =\n authorizationHeader.indexOf('error=\"invalid_grant\"') > -1;\n\n if (invalidGrant && this.#tokenStorage) {\n return this._refreshTokenAndRefetch(input, requestParams);\n }\n\n throw error;\n } else {\n // if www-authenticate header is missing, try and read json response\n return response\n .json()\n .then((body) => {\n if (\n this.#tokenStorage &&\n (body.error === 'invalid_scope' || body.error === 'access_denied')\n ) {\n return this._refreshTokenAndRefetch(input, requestParams);\n }\n throw error;\n })\n .catch((err) => {\n if (err instanceof OauthError) {\n throw err;\n }\n throw error;\n });\n }\n }\n\n private _doFetch(\n accessToken: null | string,\n input: string,\n requestParams: RequestInit\n ): Promise<Response> {\n let params = requestParams;\n let baseHeaders: HeadersInit = {};\n\n if (params.method !== 'GET' || (params.method !== 'GET' && params.method !== 'DELETE')) {\n baseHeaders = {\n 'Content-Type': 'application/json',\n };\n }\n\n if (accessToken) {\n baseHeaders.Authorization = `${this.sdk.config.authorizationType} ${accessToken}`;\n }\n\n if (params) {\n if (!params.headers) {\n params.headers = {};\n }\n\n params.headers = Object.assign(baseHeaders, params.headers);\n } else {\n params = { headers: baseHeaders };\n }\n\n if (params.headers) {\n params.headers = removeUndefinedHeaders(params.headers);\n }\n\n let logId: undefined | string;\n if (this.sdk.logger) {\n logId = this.sdk.logger.logRequest({ url: input, ...params });\n }\n\n // eslint-disable-next-line consistent-return\n return fetch(input, params).then((response) => {\n if (this.sdk.logger) {\n this.sdk.logger.logResponse(response, logId);\n }\n\n if (response.status < 400) {\n return response;\n }\n\n if (response.status === 401) {\n return this._manageUnauthorized(response, input, params);\n }\n\n const httpError = getHttpErrorFromResponse(response);\n throw httpError;\n });\n }\n\n private _getEntityIdentifier(\n object: Record<string, unknown>\n ): null | string | number {\n const idKey = this.metadata.getIdentifierAttribute().serializedKey;\n\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n return object[idKey];\n }\n\n private _handleRefreshTokenFailure(error: Error): void {\n if (error instanceof OauthError) {\n this.#tokenStorage.logout().then(() => {\n if (this.sdk.config.onRefreshTokenFailure) {\n this.sdk.config.onRefreshTokenFailure(error);\n }\n });\n }\n }\n}\n\nexport default AbstractClient;\n","import AbstractClient from '../client/AbstractClient';\nimport Attribute from './Attribute';\nimport Relation from './Relation';\n\ntype AttributeListType = { [key: string]: Attribute };\ntype RelationListType = { [key: string]: Relation };\nexport type DefaultSerializedModelType = {\n [key: string]: null | null[];\n};\n\nclass ClassMetadata {\n readonly key: string;\n\n readonly pathRoot: string;\n\n repositoryClass: typeof AbstractClient;\n\n #attributeList: AttributeListType;\n\n #relationList: RelationListType;\n\n #identifierAttribute?: Attribute;\n\n /**\n * @param {string} key mandatory, will be passed in your serializer\n * @param {string|null} pathRoot the endpoint of your API: will be added to the mapping prefix ('/v1' here)\n * @param {typeof AbstractClient} repositoryClass [Overriding repository]{@link https://github.com/mapado/rest-client-js-sdk#overriding-repository} for more detail\n */\n constructor(\n key: string,\n pathRoot: string | null = null,\n // modelName,\n repositoryClass: typeof AbstractClient = AbstractClient\n ) {\n if (!key) {\n throw new TypeError('key attribute are required');\n }\n\n this.key = key;\n this.pathRoot = pathRoot || key;\n // this.modelName = modelName;\n this.repositoryClass = repositoryClass;\n\n this.#attributeList = {};\n this.#relationList = {};\n }\n\n getAttribute(name: string): Attribute {\n return this.#attributeList[name];\n }\n\n hasIdentifierAttribute(): boolean {\n return !!this.#identifierAttribute;\n }\n\n getIdentifierAttribute(): Attribute {\n if (!this.#identifierAttribute) {\n throw new TypeError(\n `\"${this.key}\" has no identifier attribute set. Did you call \"setAttributeList\" first ?`\n );\n }\n\n return this.#identifierAttribute;\n }\n\n getAttributeList(): AttributeListType {\n return this.#attributeList;\n }\n\n setAttributeList(attributeList: Attribute[]): void {\n this.#attributeList = {};\n this.#identifierAttribute = undefined;\n\n attributeList.forEach((attribute) => {\n this.#attributeList[attribute.serializedKey] = attribute;\n\n if (attribute.isIdentifier) {\n this.#identifierAttribute = attribute;\n }\n });\n\n if (!this.#identifierAttribute) {\n throw new TypeError(\n `\"${this.key}\" has no identifier attribute set. You must set all your attributes in one time and send an attribute with \"isIdentifier=true\"`\n );\n }\n }\n\n setRelationList(relationList: Relation[]): void {\n this.#relationList = {};\n\n relationList.forEach((relation) => {\n this.#relationList[relation.serializedKey] = relation;\n this.#attributeList[relation.serializedKey] = new Attribute(\n relation.serializedKey,\n relation.serializedKey,\n relation.isRelationToMany() ? 'array' : 'object'\n );\n });\n }\n\n getRelation(key: string): Relation {\n return this.#relationList[key];\n }\n\n getDefaultSerializedModel(): DefaultSerializedModelType {\n const out: DefaultSerializedModelType = {};\n\n Object.keys(this.#attributeList).forEach((serializedKey) => {\n out[serializedKey] = null;\n });\n\n Object.keys(this.#relationList).forEach((serializedKey) => {\n const relation = this.#relationList[serializedKey];\n\n if (relation.isRelationToMany()) {\n out[serializedKey] = [];\n }\n });\n\n return out;\n }\n}\n\nexport default ClassMetadata;\n","enum RelationTypes {\n ONE_TO_ONE = 'ONE_TO_ONE',\n ONE_TO_MANY = 'ONE_TO_MANY',\n MANY_TO_MANY = 'MANY_TO_MANY',\n MANY_TO_ONE = 'MANY_TO_ONE',\n}\n\nclass Relation {\n public static ONE_TO_ONE = RelationTypes.ONE_TO_ONE;\n\n public static ONE_TO_MANY = RelationTypes.ONE_TO_MANY;\n\n public static MANY_TO_ONE = RelationTypes.MANY_TO_ONE;\n\n public static MANY_TO_MANY = RelationTypes.MANY_TO_MANY;\n\n type: RelationTypes;\n\n readonly targetMetadataKey: string;\n\n readonly serializedKey: string;\n\n readonly attributeName: string;\n\n /**\n * @param {RelationTypes} type the type of relation\n * @param {string} targetMetadataKey must match the first argument of `ClassMetadata` constructor of the target entity\n * @param {string} serializedKey the key returned from your API\n * @param {string|null} attributeName the name in your entity, default to the `serializedKey` attribute\n */\n constructor(\n type: RelationTypes,\n targetMetadataKey: string,\n serializedKey: string,\n attributeName: string | null = null\n ) {\n this.type = type;\n this.targetMetadataKey = targetMetadataKey;\n this.serializedKey = serializedKey;\n this.attributeName = attributeName || this.serializedKey;\n }\n\n isOneToOne(): boolean {\n return this.type === Relation.ONE_TO_ONE;\n }\n\n isOneToMany(): boolean {\n return this.type === Relation.ONE_TO_MANY;\n }\n\n isManyToOne(): boolean {\n return this.type === Relation.MANY_TO_ONE;\n }\n\n isManyToMany(): boolean {\n return this.type === Relation.MANY_TO_MANY;\n }\n\n isRelationToMany(): boolean {\n return this.isManyToMany() || this.isOneToMany();\n }\n\n isRelationToOne(): boolean {\n return this.isManyToOne() || this.isOneToOne();\n }\n}\n\nexport default Relation;\n","/* eslint-disable */\n// \"fork\" of https://github.com/facebook/immutable-js/blob/v4.0.0-rc.9/src/Predicates.js\n// because tree-shaking does not seems to work as expected\n\n/**\n * Copyright (c) 2014-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nexport const IS_ITERABLE_SENTINEL = '@@__IMMUTABLE_ITERABLE__@@';\nexport const IS_KEYED_SENTINEL = '@@__IMMUTABLE_KEYED__@@';\nexport const IS_INDEXED_SENTINEL = '@@__IMMUTABLE_INDEXED__@@';\nexport const IS_ORDERED_SENTINEL = '@@__IMMUTABLE_ORDERED__@@';\nexport const IS_RECORD_SENTINEL = '@@__IMMUTABLE_RECORD__@@';\n\nexport function isImmutable(maybeImmutable: unknown): boolean {\n return isCollection(maybeImmutable) || isRecord(maybeImmutable);\n}\n\nexport function isCollection(maybeCollection: any): boolean {\n return !!(maybeCollection && maybeCollection[IS_ITERABLE_SENTINEL]);\n}\n\nexport function isKeyed(maybeKeyed: any): boolean {\n return !!(maybeKeyed && maybeKeyed[IS_KEYED_SENTINEL]);\n}\n\nexport function isIndexed(maybeIndexed: any): boolean {\n return !!(maybeIndexed && maybeIndexed[IS_INDEXED_SENTINEL]);\n}\n\nexport function isAssociative(maybeAssociative: any): boolean {\n return isKeyed(maybeAssociative) || isIndexed(maybeAssociative);\n}\n\nexport function isOrdered(maybeOrdered: any): boolean {\n return !!(maybeOrdered && maybeOrdered[IS_ORDERED_SENTINEL]);\n}\n\nexport function isRecord(maybeRecord: any): boolean {\n return !!(maybeRecord && maybeRecord[IS_RECORD_SENTINEL]);\n}\n\nexport function isValueObject(maybeValue: any): boolean {\n return !!(\n maybeValue &&\n typeof maybeValue.equals === 'function' &&\n typeof maybeValue.hashCode === 'function'\n );\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport diff from 'deep-diff';\nimport Mapping from './Mapping';\nimport Attribute from './Mapping/Attribute';\nimport ClassMetadata, {\n DefaultSerializedModelType,\n} from './Mapping/ClassMetadata';\nimport { isImmutable } from './isImmutable';\n\ntype Id = string | number;\ntype StringKeyObject = Record<string, any>;\n\n/**\n * deep comparaison between objects\n */\nfunction objectDiffers(\n left: Record<string, unknown>,\n right: Record<string, unknown>\n): boolean {\n const result = diff(left, right);\n\n return result ? result.length > 0 : false;\n}\n\n/**\n * get the id of an entity or return itself if string\n */\nfunction getEntityId(\n stringOrEntity: string | Record<string, unknown>,\n idSerializedKey: string\n): Id {\n if (typeof stringOrEntity !== 'object') {\n return stringOrEntity;\n }\n\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n return stringOrEntity[idSerializedKey] as Id;\n}\n\n/**\n * find old relation value from the new relation id\n * if not found, returs the default serilazed model\n */\nfunction findOldRelation(\n newRelationValue: any[],\n oldRelationValue: any[],\n classMetadata: ClassMetadata\n): DefaultSerializedModelType {\n const idSerializedKey = classMetadata.getIdentifierAttribute().serializedKey;\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n const relationValueId = getEntityId(newRelationValue, idSerializedKey);\n\n const foundValue =\n oldRelationValue &&\n oldRelationValue.find((innerOldRelationValue) => {\n const oldRelationValueId = getEntityId(\n innerOldRelationValue,\n idSerializedKey\n );\n\n return relationValueId === oldRelationValueId;\n });\n\n if (foundValue) {\n return foundValue;\n }\n\n return classMetadata.getDefaultSerializedModel();\n}\n\n/**\n * add all identifier from one to many relation list\n */\nfunction getIdentifierForList(\n newValue: any[],\n idSerializedKey: string\n): StringKeyObject | string {\n return newValue.map((value) => {\n if (Object.keys(value).includes(idSerializedKey)) {\n return {\n [idSerializedKey]: value[idSerializedKey],\n };\n }\n\n if (typeof value === 'string') {\n return value;\n }\n\n throw new TypeError(\n 'new value should include a list of string or objects containing the serialized key'\n );\n });\n}\n\nclass UnitOfWork {\n mapping: Mapping;\n\n #storage: { [key in Id]: Record<string, unknown> };\n\n #enabled: boolean;\n\n constructor(mapping: Mapping, enabled = false) {\n this.mapping = mapping;\n\n this.#enabled = enabled;\n this.#storage = {};\n }\n\n registerClean(id: Id, entity: Record<string, unknown>): void {\n if (!this.#enabled) {\n return;\n }\n if (isImmutable(entity)) {\n this.#storage[id] = entity;\n } else {\n this.#storage[id] = { ...entity };\n }\n }\n\n getDirtyEntity(id: Id): Record<string, unknown> {\n return this.#storage[id];\n }\n\n clear(id: Id): void {\n delete this.#storage[id];\n }\n\n getDirtyData(\n newSerializedModel: Record<string, unknown>,\n oldSerializedModel: Record<string, unknown>,\n classMetadata: ClassMetadata\n ): StringKeyObject {\n return this._getDirtyFields(\n newSerializedModel,\n oldSerializedModel,\n classMetadata\n );\n }\n\n private _getDirtyFieldsForAttribute(\n dirtyFieldsParam: StringKeyObject,\n key: string,\n attribute: Attribute,\n oldValue: Record<string, unknown>,\n newValue: Record<string, unknown>\n ): StringKeyObject {\n const dirtyFields = dirtyFieldsParam;\n\n if (attribute.type === 'object') {\n if (oldValue === undefined || objectDiffers(oldValue, newValue)) {\n dirtyFields[key] = newValue;\n }\n } else if (oldValue !== newValue) {\n dirtyFields[key] = newValue;\n }\n\n return dirtyFields;\n }\n\n private _getDirtyFieldsForManyToOne(\n dirtyFieldsParam: StringKeyObject,\n key: string,\n oldValue: Record<string, unknown>,\n newValue: Record<string, unknown>,\n relationMetadata: ClassMetadata,\n idSerializedKey: string\n ): StringKeyObject {\n const dirtyFields = dirtyFieldsParam;\n if (oldValue !== newValue) {\n if (newValue === null) {\n dirtyFields[key] = null;\n\n return dirtyFields;\n }\n\n if (typeof oldValue === 'string' || typeof newValue === 'string') {\n dirtyFields[key] = newValue;\n }\n\n const recursiveDiff = this._getDirtyFields(\n newValue,\n oldValue,\n relationMetadata\n );\n\n if (Object.keys(recursiveDiff).length > 0) {\n recursiveDiff[idSerializedKey] = getEntityId(newValue, idSerializedKey);\n dirtyFields[key] = recursiveDiff;\n }\n }\n\n return dirtyFields;\n }\n\n private _getDirtyFieldsForOneToMany(\n dirtyFieldsParam: StringKeyObject,\n key: string,\n idSerializedKey: string,\n relationMetadata: ClassMetadata,\n oldValue: any[],\n newValue: any[]\n ): StringKeyObject {\n const dirtyFields = dirtyFieldsParam;\n const newValueLength = newValue ? newValue.length : 0;\n const oldValueLength = oldValue ? oldValue.length : 0;\n\n // number of items changed\n if (newValueLength !== oldValueLength) {\n dirtyFields[key] = getIdentifierForList(newValue, idSerializedKey);\n }\n\n if (newValue && newValue.length > 0) {\n if (dirtyFields[key] === undefined) {\n dirtyFields[key] = [];\n }\n\n const relationList = newValue;\n\n relationList.forEach((newRelationValue, relationAttributeName) => {\n const oldRelationValue = findOldRelation(\n newRelationValue,\n oldValue,\n relationMetadata\n );\n\n if (newRelationValue !== oldRelationValue) {\n if (\n typeof newRelationValue === 'string' ||\n typeof oldRelationValue === 'string'\n ) {\n dirtyFields[key][relationAttributeName] = newRelationValue;\n\n return;\n }\n\n const recursiveDiff = this._getDirtyFields(\n newRelationValue,\n oldRelationValue,\n relationMetadata\n );\n\n if (Object.keys(recursiveDiff).length > 0) {\n const entityId = getEntityId(newRelationValue, idSerializedKey);\n\n if (entityId !== null) {\n recursiveDiff[idSerializedKey] = entityId;\n }\n\n if (dirtyFields[key][relationAttributeName]) {\n Object.assign(\n dirtyFields[key][relationAttributeName],\n recursiveDiff\n );\n } else {\n dirtyFields[key][relationAttributeName] = recursiveDiff;\n }\n }\n }\n });\n\n if (dirtyFields[key].length > 0) {\n // add identifier because one or more object linked has been updated\n // see test: 'get dirty data many to one update item'\n dirtyFields[key] = Object.assign(\n getIdentifierForList(newValue, idSerializedKey),\n dirtyFields[key]\n );\n } else {\n // no changes, no need to send the key with an empty array as value\n delete dirtyFields[key];\n }\n }\n\n return dirtyFields;\n }\n\n private _getDirtyFields(\n newSerializedModel: StringKeyObject,\n oldSerializedModel: StringKeyObject,\n classMetadata: ClassMetadata\n ): StringKeyObject {\n let dirtyFields = {};\n\n Object.values(classMetadata.getAttributeList()).forEach((attribute) => {\n const key = attribute.attributeName;\n const newValue = newSerializedModel[key];\n const oldValue = oldSerializedModel ? oldSerializedModel[key] : null;\n\n const currentRelation = classMetadata.getRelation(\n attribute.serializedKey\n );\n\n if (newValue === undefined) {\n return;\n }\n\n // not a relation\n if (!currentRelation) {\n dirtyFields = this._getDirtyFieldsForAttribute(\n dirtyFields,\n key,\n attribute,\n oldValue,\n newValue\n );\n\n return;\n }\n\n const relationMetadata = this.mapping.getClassMetadataByKey(\n currentRelation.targetMetadataKey\n );\n\n if (!relationMetadata) {\n throw new TypeError(\n `relation metadata is not set for relation ${classMetadata.key}.${currentRelation.targetMetadataKey}`\n );\n }\n\n const idSerializedKey = relationMetadata.getIdentifierAttribute()\n .serializedKey;\n\n // MANY_TO_ONE relation\n if (currentRelation.isRelationToOne()) {\n dirtyFields = this._getDirtyFieldsForManyToOne(\n dirtyFields,\n key,\n oldValue,\n newValue,\n relationMetadata,\n idSerializedKey\n );\n\n return;\n }\n\n // *_TO_MANY relation\n dirtyFields = this._getDirtyFieldsForOneToMany(\n dirtyFields,\n key,\n idSerializedKey,\n relationMetadata,\n oldValue,\n newValue\n );\n });\n\n return dirtyFields;\n }\n}\n\nexport default UnitOfWork;\n","/* eslint-disable @typescript-eslint/no-unused-vars */\nimport ClassMetadata from '../Mapping/ClassMetadata';\nimport SerializerInterface, {\n Entity,\n NormalizedObject,\n NormalizedList,\n EntityList,\n} from './SerializerInterface';\n\nclass Serializer implements SerializerInterface {\n /**\n * convert an entity to a plain javascript object\n * @param {object} entity - The entity to convert\n * @param {ClassMetadata} classMetadata - the class metadata\n * @return {object} the object to serialize\n */\n normalizeItem(\n entity: Entity,\n classMetadata: ClassMetadata\n ): NormalizedObject {\n return entity;\n }\n\n /**\n * convert a plain javascript object to string\n * @param {object} object - The object to convert to convert\n * @param {ClassMetadata} classMetadata - the class metadata\n * @return {string} the content of the request\n */\n encodeItem(object: NormalizedObject, classMetadata: ClassMetadata): string {\n throw new TypeError('`encodeItem` method must be implemented');\n }\n\n /**\n * convert an entity to string that will be sent as the request content\n * @param {any} entity - The entity to convert\n * @param {ClassMetadata} classMetadata - the class metadata\n * @return {string} the content of the request\n */\n serializeItem(object: Entity, classMetadata: ClassMetadata): string {\n const noralizedData = this.normalizeItem(object, classMetadata);\n return this.encodeItem(noralizedData, classMetadata);\n }\n\n /**\n * convert a plain object to an entity\n * @param {object} object - The plain javascript object\n * @param {ClassMetadata} classMetadata - the c