jsona
Version:
Provide data formatters (data model builder & json builder) to work with JSON API specification v1.0 in your JavaScript / TypeScript code
244 lines (198 loc) • 8.12 kB
text/typescript
import {
TJsonaModel,
TJsonApiBody,
TJsonApiData,
TJsonaDenormalizedIncludeNames,
TJsonaNormalizedIncludeNamesTree,
TJsonaUniqueIncluded,
IModelPropertiesMapper,
IModelsSerializer,
} from '../JsonaTypes';
import {createIncludeNamesTree} from '../utils';
export class ModelsSerializer implements IModelsSerializer {
protected propertiesMapper: IModelPropertiesMapper;
protected stuff: TJsonaModel | Array<TJsonaModel>;
protected includeNamesTree: TJsonaNormalizedIncludeNamesTree;
private buildIncludedIndex: number;
constructor(propertiesMapper?: IModelPropertiesMapper) {
propertiesMapper && this.setPropertiesMapper(propertiesMapper);
this.buildIncludedIndex = 0;
}
setPropertiesMapper(propertiesMapper: IModelPropertiesMapper) {
this.propertiesMapper = propertiesMapper;
}
setStuff(stuff: TJsonaModel | TJsonaModel[]) {
this.stuff = stuff;
}
setIncludeNames(includeNames: TJsonaDenormalizedIncludeNames | TJsonaNormalizedIncludeNamesTree) {
if (Array.isArray(includeNames)) {
const includeNamesTree = {};
includeNames.forEach((namesChain) => {
createIncludeNamesTree(namesChain, includeNamesTree);
});
this.includeNamesTree = includeNamesTree;
} else {
this.includeNamesTree = includeNames;
}
}
build(): TJsonApiBody {
const {stuff, propertiesMapper} = this;
if (!propertiesMapper || typeof propertiesMapper !== 'object') {
throw new Error('ModelsSerializer cannot build, propertiesMapper is not set');
} else if (!stuff || typeof stuff !== 'object') {
throw new Error('ModelsSerializer cannot build, stuff is not set');
}
const body: TJsonApiBody = {};
const uniqueIncluded: TJsonaUniqueIncluded = {};
if (stuff && Array.isArray(stuff)) {
const collectionLength = stuff.length;
const data = [];
for (let i = 0; i < collectionLength; i++) {
data.push(
this.buildDataByModel(stuff[i])
);
this.buildIncludedByModel(
stuff[i],
this.includeNamesTree,
uniqueIncluded
);
}
body['data'] = data;
} else if (stuff) {
body['data'] = this.buildDataByModel(stuff);
this.buildIncludedByModel(
stuff,
this.includeNamesTree,
uniqueIncluded
);
} else if (stuff === null) {
body['data'] = null;
}
if (Object.keys(uniqueIncluded).length) {
body['included'] = Object.values(uniqueIncluded);
}
return body;
}
buildDataByModel(model: TJsonaModel | null): TJsonApiData {
const id = this.propertiesMapper.getId(model);
const type = this.propertiesMapper.getType(model);
const attributes = this.propertiesMapper.getAttributes(model);
const data = { type,
...(typeof id !== 'undefined' ? { id } : {}),
...(typeof attributes !== 'undefined' ? { attributes } : {}),
};
if (typeof data.type !== 'string' || !data.type) {
console.warn('ModelsSerializer cannot buildDataByModel, type is not set or incorrect', model);
throw new Error('ModelsSerializer cannot buildDataByModel, type is not set or incorrect');
}
const relationships = this.buildRelationshipsByModel(model);
if (relationships && Object.keys(relationships).length) {
data['relationships'] = relationships;
}
return data;
}
buildResourceObjectPart(relation: TJsonaModel) {
const id = this.propertiesMapper.getId(relation);
const type = this.propertiesMapper.getType(relation);
return {
type,
...(typeof id === 'undefined' ? {} : { id }),
};
}
buildRelationshipsByModel(model: TJsonaModel) {
const relations = this.propertiesMapper.getRelationships(model);
if (!relations || !Object.keys(relations).length) {
return;
}
const relationships = {};
Object.keys(relations).forEach((k) => {
const relation = relations[k];
if (Array.isArray(relation)) {
const relationshipData = [];
for (const relationItem of relation) {
const relationshipDataItem = this.buildResourceObjectPart(relationItem);
if ('type' in relationshipDataItem) {
relationshipData.push(relationshipDataItem);
} else {
console.error(
`Can't create data item for relationship ${k},
it doesn't have 'id' or 'type', it was skipped`,
relationItem
);
}
}
relationships[k] = {
data: relationshipData
};
} else if (relation) {
const item = this.buildResourceObjectPart(relation);
if ('type' in item) {
relationships[k] = {
data: item
};
} else {
console.error(
`Can't create data for relationship ${k}, it doesn't have 'type', it was skipped`,
relation
);
}
} else {
relationships[k] = {
data: relation
};
}
});
return relationships;
}
buildIncludedByModel(
model: TJsonaModel,
includeTree: TJsonaNormalizedIncludeNamesTree,
builtIncluded: TJsonaUniqueIncluded = {}
): void {
if (!includeTree || !Object.keys(includeTree).length) {
return;
}
const modelRelationships = this.propertiesMapper.getRelationships(model);
if (!modelRelationships || !Object.keys(modelRelationships).length) {
return;
}
const includeNames = Object.keys(includeTree);
const includeNamesLength = includeNames.length;
for (let i = 0; i < includeNamesLength; i++) {
const currentRelationName = includeNames[i];
const relation: TJsonaModel | Array<TJsonaModel> = modelRelationships[currentRelationName];
if (relation) {
if (Array.isArray(relation)) {
let relationModelsLength = relation.length;
for (let r = 0; r < relationModelsLength; r++) {
const relationModel: TJsonaModel = relation[r];
this.buildIncludedItem(relationModel, includeTree[currentRelationName], builtIncluded);
}
} else {
this.buildIncludedItem(relation, includeTree[currentRelationName], builtIncluded);
}
}
}
}
buildIncludedItem(
relationModel: TJsonaModel,
subIncludeTree: TJsonaNormalizedIncludeNamesTree,
builtIncluded: TJsonaUniqueIncluded
) {
const id = this.propertiesMapper.getId(relationModel);
const type = this.propertiesMapper.getType(relationModel);
let includeKey = type + id;
if (!id || !builtIncluded[includeKey]) {
// create data by current entity if such included is not yet created
if (includeKey in builtIncluded) {
includeKey += this.buildIncludedIndex;
this.buildIncludedIndex += 1;
}
builtIncluded[includeKey] = this.buildDataByModel(relationModel);
if (subIncludeTree) {
this.buildIncludedByModel(relationModel, subIncludeTree, builtIncluded);
}
}
}
}
export default ModelsSerializer;