@orbit/jsonapi
Version:
JSON:API support for Orbit.
681 lines (585 loc) • 18.5 kB
text/typescript
import { deepSet, Dict } from '@orbit/utils';
import { Orbit, Assertion } from '@orbit/core';
import {
RecordSchema,
RecordKeyMap,
InitializedRecord,
RecordIdentity,
RecordOperation,
ModelDefinition
} from '@orbit/records';
import {
Serializer,
SerializerForFn,
StringSerializer,
buildSerializerSettingsFor
} from '@orbit/serializers';
import {
Resource,
ResourceDocument,
ResourceIdentity,
ResourceRelationship
} from './resource-document';
import {
ResourceAtomicOperation,
RecordOperationsDocument
} from './resource-operations';
import { ResourceAtomicOperationsDocument } from './resource-operations';
import { RecordDocument } from './record-document';
import { JSONAPIResourceSerializer } from './serializers/jsonapi-resource-serializer';
import { JSONAPIResourceIdentitySerializer } from './serializers/jsonapi-resource-identity-serializer';
import { buildJSONAPISerializerFor } from './serializers/jsonapi-serializer-builder';
import { JSONAPISerializers } from './serializers/jsonapi-serializers';
import { JSONAPIAtomicOperationSerializer } from './serializers/jsonapi-atomic-operation-serializer';
import { JSONAPIResourceFieldSerializer } from './serializers/jsonapi-resource-field-serializer';
const { deprecate } = Orbit;
export interface JSONAPISerializationOptions {
primaryRecord?: InitializedRecord;
primaryRecords?: InitializedRecord[];
}
export interface JSONAPISerializerSettings {
schema: RecordSchema;
keyMap?: RecordKeyMap;
serializers?: Dict<Serializer>;
}
/**
* @deprecated since v0.17, remove in v0.18
*/
export class JSONAPISerializer
implements
Serializer<
RecordDocument,
ResourceDocument,
JSONAPISerializationOptions,
JSONAPISerializationOptions
> {
protected _schema: RecordSchema;
protected _keyMap?: RecordKeyMap;
protected _serializerFor: SerializerForFn;
constructor(settings: JSONAPISerializerSettings) {
deprecate(
"The 'JSONAPISerializer' class has deprecated. Use 'serializerFor' instead."
);
const { schema, keyMap, serializers } = settings;
let serializerFor: SerializerForFn | undefined;
if (serializers) {
serializerFor = (type: string) => serializers[type];
}
const serializerSettingsFor = buildSerializerSettingsFor({
settingsByType: {
[JSONAPISerializers.ResourceField]: {
serializationOptions: { inflectors: ['dasherize'] }
},
[JSONAPISerializers.ResourceType]: {
serializationOptions: { inflectors: ['pluralize', 'dasherize'] }
}
}
});
this._schema = schema;
this._keyMap = keyMap;
this._serializerFor = buildJSONAPISerializerFor({
schema,
keyMap,
serializerFor,
serializerSettingsFor
});
}
get schema(): RecordSchema {
return this._schema;
}
get keyMap(): RecordKeyMap | undefined {
return this._keyMap;
}
get serializerFor(): SerializerForFn {
return this._serializerFor;
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
resourceKey(type: string): string {
return 'id';
}
resourceType(type: string): string {
return this.typeSerializer.serialize(type) as string;
}
resourceRelationship(type: string | undefined, relationship: string): string {
return this.fieldSerializer.serialize(relationship, { type }) as string;
}
resourceAttribute(type: string | undefined, attr: string): string {
return this.fieldSerializer.serialize(attr, { type }) as string;
}
resourceIdentity(identity: RecordIdentity): Resource {
return {
type: this.resourceType(identity.type),
id: this.resourceId(identity.type, identity.id)
};
}
resourceIds(type: string, ids: string[]): (string | undefined)[] {
return ids.map((id) => this.resourceId(type, id));
}
resourceId(type: string, id: string): string | undefined {
let resourceKey = this.resourceKey(type);
if (resourceKey === 'id') {
return id;
} else if (this.keyMap) {
return this.keyMap.idToKey(type, resourceKey, id);
} else {
throw new Assertion(
`A keyMap is required to determine an id from the key '${resourceKey}'`
);
}
}
recordId(type: string, resourceId: string): string {
let resourceKey = this.resourceKey(type);
if (resourceKey === 'id') {
return resourceId;
}
let existingId;
if (this.keyMap) {
existingId = this.keyMap.keyToId(type, resourceKey, resourceId);
if (existingId) {
return existingId;
}
} else {
throw new Assertion(
`A keyMap is required to determine an id from the key '${resourceKey}'`
);
}
return this._generateNewId(type, resourceKey, resourceId);
}
recordType(resourceType: string): string {
return this.typeSerializer.deserialize(resourceType) as string;
}
recordIdentity(resourceIdentity: ResourceIdentity): RecordIdentity {
let type = this.recordType(resourceIdentity.type);
let id = this.recordId(type, resourceIdentity.id);
return { type, id };
}
recordAttribute(type: string, resourceAttribute: string): string {
return this.fieldSerializer.deserialize(resourceAttribute) as string;
}
recordRelationship(type: string, resourceRelationship: string): string {
return this.fieldSerializer.deserialize(resourceRelationship) as string;
}
serialize(document: RecordDocument): ResourceDocument {
let data = document.data;
return {
data: Array.isArray(data)
? this.serializeRecords(data as InitializedRecord[])
: this.serializeRecord(data as InitializedRecord)
};
}
serializeAtomicOperationsDocument(
document: RecordOperationsDocument
): ResourceAtomicOperationsDocument {
return {
'atomic:operations': this.serializeAtomicOperations(document.operations)
};
}
serializeAtomicOperations(
operations: RecordOperation[]
): ResourceAtomicOperation[] {
return operations.map((operation) =>
this.serializeAtomicOperation(operation)
);
}
serializeAtomicOperation(
operation: RecordOperation
): ResourceAtomicOperation {
return this.atomicOperationSerializer.serialize(operation);
}
serializeRecords(records: InitializedRecord[]): Resource[] {
return records.map((record) => this.serializeRecord(record));
}
serializeRecord(record: InitializedRecord): Resource {
const resource: Resource = {
type: this.resourceType(record.type)
};
const model: ModelDefinition = this._schema.getModel(record.type);
this.serializeId(resource, record, model);
this.serializeAttributes(resource, record, model);
this.serializeRelationships(resource, record, model);
return resource;
}
serializeIdentity(record: InitializedRecord): Resource {
return {
type: this.resourceType(record.type),
id: this.resourceId(record.type, record.id)
};
}
serializeId(
resource: Resource,
record: RecordIdentity,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
model: ModelDefinition
): void {
let value = this.resourceId(record.type, record.id);
if (value !== undefined) {
resource.id = value;
}
}
serializeAttributes(
resource: Resource,
record: InitializedRecord,
model: ModelDefinition
): void {
if (record.attributes) {
Object.keys(record.attributes).forEach((attr) => {
this.serializeAttribute(resource, record, attr, model);
});
}
}
serializeAttribute(
resource: Resource,
record: InitializedRecord,
attr: string,
model: ModelDefinition
): void {
let value: any = record.attributes?.[attr];
if (value === undefined) {
return;
}
const attrOptions = model.attributes?.[attr];
if (attrOptions === undefined) {
return;
}
const serializer = this.serializerFor(attrOptions.type || 'unknown');
if (serializer) {
const serializationOptions =
attrOptions.serialization ?? (attrOptions as any).serializationOptions;
if ((attrOptions as any).serializationOptions !== undefined) {
deprecate(
`The attribute '${attr}' for '${record.type}' has been assigned \`serializationOptions\` in the schema. Use \`serialization\` instead.`
);
}
value =
value === null
? null
: serializer.serialize(value, serializationOptions);
}
deepSet(
resource,
['attributes', this.resourceAttribute(record.type, attr)],
value
);
}
serializeRelationships(
resource: Resource,
record: InitializedRecord,
model: ModelDefinition
): void {
if (record.relationships) {
Object.keys(record.relationships).forEach((relationship) => {
this.serializeRelationship(resource, record, relationship, model);
});
}
}
serializeRelationship(
resource: Resource,
record: InitializedRecord,
relationship: string,
model: ModelDefinition
): void {
const value = record.relationships?.[relationship].data;
if (value === undefined) {
return;
}
if (model.relationships?.[relationship] === undefined) {
return;
}
let data;
if (Array.isArray(value)) {
data = (value as RecordIdentity[]).map((id) => this.resourceIdentity(id));
} else if (value !== null) {
data = this.resourceIdentity(value as RecordIdentity);
} else {
data = null;
}
const resourceRelationship = this.resourceRelationship(
record.type,
relationship
);
deepSet(resource, ['relationships', resourceRelationship, 'data'], data);
}
deserialize(
document: ResourceDocument,
options?: JSONAPISerializationOptions
): RecordDocument {
let result: RecordDocument;
let data;
if (Array.isArray(document.data)) {
let primaryRecords = options?.primaryRecords;
if (primaryRecords) {
data = (document.data as Resource[]).map((entry, i) => {
return this.deserializeResource(entry, primaryRecords?.[i]);
});
} else {
data = (document.data as Resource[]).map((entry) =>
this.deserializeResource(entry)
);
}
} else if (document.data !== null) {
let primaryRecord = options && options.primaryRecord;
if (primaryRecord) {
data = this.deserializeResource(
document.data as Resource,
primaryRecord
);
} else {
data = this.deserializeResource(document.data as Resource);
}
} else {
data = null;
}
result = { data };
if (document.included) {
result.included = document.included.map((e) =>
this.deserializeResource(e)
);
}
if (document.links) {
result.links = document.links;
}
if (document.meta) {
result.meta = document.meta;
}
return result;
}
deserializeAtomicOperationsDocument(
document: ResourceAtomicOperationsDocument
): RecordOperationsDocument {
const result: RecordOperationsDocument = {
operations: this.deserializeAtomicOperations(
document['atomic:operations']
)
};
if (document.links) {
result.links = document.links;
}
if (document.meta) {
result.meta = document.meta;
}
return result;
}
deserializeAtomicOperations(
operations: ResourceAtomicOperation[]
): RecordOperation[] {
return operations.map((operation) =>
this.deserializeAtomicOperation(operation)
);
}
deserializeAtomicOperation(
operation: ResourceAtomicOperation
): RecordOperation {
return this.atomicOperationSerializer.deserialize(operation);
}
deserializeResourceIdentity(
resource: Resource,
primaryRecord?: InitializedRecord
): InitializedRecord {
let record: InitializedRecord;
const type: string = this.recordType(resource.type);
const resourceKey = this.resourceKey(type);
if (resourceKey === 'id') {
if (resource.id) {
record = { type, id: resource.id };
} else {
throw new Assertion(`A resource has been enountered without an id`);
}
} else if (this.keyMap) {
let id: string;
let keys: Dict<string> | undefined;
if (resource.id) {
keys = {
[resourceKey]: resource.id
};
id =
(primaryRecord && primaryRecord.id) ||
this.keyMap.idFromKeys(type, keys) ||
this.schema.generateId(type);
} else {
id =
(primaryRecord && primaryRecord.id) || this.schema.generateId(type);
}
record = { type, id };
if (keys) {
record.keys = keys;
}
} else {
throw new Assertion(
`A keyMap is required to determine an id from the key '${resourceKey}'`
);
}
if (this.keyMap) {
this.keyMap.pushRecord(record);
}
return record;
}
deserializeResource(
resource: Resource,
primaryRecord?: InitializedRecord
): InitializedRecord {
const record = this.deserializeResourceIdentity(resource, primaryRecord);
const model: ModelDefinition = this._schema.getModel(record.type);
this.deserializeAttributes(record, resource, model);
this.deserializeRelationships(record, resource, model);
this.deserializeLinks(record, resource, model);
this.deserializeMeta(record, resource, model);
return record;
}
deserializeAttributes(
record: InitializedRecord,
resource: Resource,
model: ModelDefinition
): void {
if (resource.attributes) {
Object.keys(resource.attributes).forEach((resourceAttribute) => {
let attribute = this.recordAttribute(record.type, resourceAttribute);
if (this.schema.hasAttribute(record.type, attribute)) {
let value = resource.attributes?.[resourceAttribute];
if (value !== undefined) {
this.deserializeAttribute(record, attribute, value, model);
}
}
});
}
}
deserializeAttribute(
record: InitializedRecord,
attr: string,
value: unknown,
model: ModelDefinition
): void {
record.attributes = record.attributes || {};
if (value !== undefined && value !== null) {
const attrOptions = model.attributes?.[attr];
if (attrOptions === undefined) {
return;
}
const serializer = this.serializerFor(attrOptions?.type || 'unknown');
if (serializer) {
const deserializationOptions =
attrOptions.deserialization ??
(attrOptions as any).deserializationOptions;
if ((attrOptions as any).deserializationOptions !== undefined) {
deprecate(
`The attribute '${attr}' for '${record.type}' has been assigned \`deserializationOptions\` in the schema. Use \`deserialization\` instead.`
);
}
value = serializer.deserialize(value, deserializationOptions);
}
}
record.attributes[attr] = value;
}
deserializeRelationships(
record: InitializedRecord,
resource: Resource,
model: ModelDefinition
): void {
if (resource.relationships) {
Object.keys(resource.relationships).forEach((resourceRel) => {
let relationship = this.recordRelationship(record.type, resourceRel);
if (this.schema.hasRelationship(record.type, relationship)) {
let value = resource.relationships?.[resourceRel];
if (value !== undefined) {
this.deserializeRelationship(record, relationship, value, model);
}
}
});
}
}
deserializeRelationship(
record: InitializedRecord,
relationship: string,
value: ResourceRelationship,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
model: ModelDefinition
): void {
let resourceData = value.data;
if (resourceData !== undefined) {
let data;
if (resourceData === null) {
data = null;
} else if (Array.isArray(resourceData)) {
data = (resourceData as ResourceIdentity[]).map((resourceIdentity) =>
this.recordIdentity(resourceIdentity)
);
} else {
data = this.recordIdentity(resourceData as ResourceIdentity);
}
deepSet(record, ['relationships', relationship, 'data'], data);
}
let { links, meta } = value;
if (links !== undefined) {
deepSet(record, ['relationships', relationship, 'links'], links);
}
if (meta !== undefined) {
deepSet(record, ['relationships', relationship, 'meta'], meta);
}
}
deserializeLinks(
record: InitializedRecord,
resource: Resource,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
model: ModelDefinition
): void {
if (resource.links) {
record.links = resource.links;
}
}
deserializeMeta(
record: InitializedRecord,
resource: Resource,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
model: ModelDefinition
): void {
if (resource.meta) {
record.meta = resource.meta;
}
}
// Protected / Private
protected get resourceSerializer(): JSONAPIResourceSerializer {
return this.serializerFor(
JSONAPISerializers.Resource
) as JSONAPIResourceSerializer;
}
protected get identitySerializer(): JSONAPIResourceIdentitySerializer {
return this.serializerFor(
JSONAPISerializers.ResourceIdentity
) as JSONAPIResourceIdentitySerializer;
}
protected get typeSerializer(): StringSerializer {
return this.serializerFor(
JSONAPISerializers.ResourceType
) as StringSerializer;
}
protected get fieldSerializer(): JSONAPIResourceFieldSerializer {
return this.serializerFor(
JSONAPISerializers.ResourceField
) as JSONAPIResourceFieldSerializer;
}
protected get atomicOperationSerializer(): JSONAPIAtomicOperationSerializer {
return this.serializerFor(
JSONAPISerializers.ResourceAtomicOperation
) as JSONAPIAtomicOperationSerializer;
}
protected _generateNewId(
type: string,
keyName: string,
keyValue: string
): string {
let id = this.schema.generateId(type);
if (this.keyMap) {
this.keyMap.pushRecord({
type,
id,
keys: {
[keyName]: keyValue
}
});
} else {
throw new Assertion(
`A keyMap is required to generate ids for resource type '${type}'`
);
}
return id;
}
}