Skip to content

Commit

Permalink
Fix nested relationships in included
Browse files Browse the repository at this point in the history
  • Loading branch information
jlbelanger committed Feb 24, 2024
1 parent ad92a79 commit 19b0791
Show file tree
Hide file tree
Showing 9 changed files with 985 additions and 629 deletions.
492 changes: 260 additions & 232 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

492 changes: 260 additions & 232 deletions dist/index.modern.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.modern.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/js/Helpers/Api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deserialize } from './JsonApi';
import { deserialize } from './JsonApiDeserialize';
import { trackPromise } from 'react-promise-tracker';

export default class Api {
Expand Down
274 changes: 115 additions & 159 deletions src/js/Helpers/JsonApi.js
Original file line number Diff line number Diff line change
@@ -1,208 +1,164 @@
import get from 'get-value';
import set from 'set-value';

const findIncluded = (included, id, type, mainRecord) => {
if (mainRecord && id === mainRecord.id && type === mainRecord.type) {
const output = {
id: mainRecord.id,
type: mainRecord.type,
};
if (Object.prototype.hasOwnProperty.call(mainRecord, 'attributes')) {
output.attributes = mainRecord.attributes;
}
if (Object.prototype.hasOwnProperty.call(mainRecord, 'meta')) {
output.meta = mainRecord.meta;
}
return output;
export const getSimpleRecord = (record) => ({
id: record.id,
type: record.type,
});

export const cleanRelationship = (values) => {
if (Array.isArray(values)) {
return values.map((value) => getSimpleRecord(value));
}
return included.find((data) => (data.id === id && data.type === type));
return getSimpleRecord(values);
};

const deserializeSingle = (data, otherRows = [], included = [], mainRecord = null) => {
if (!data) {
return data;
export const cleanRecord = (record) => {
const hasAttributes = Object.keys(record.attributes).length > 0;
if (!hasAttributes) {
delete record.attributes;
}
const output = {
id: data.id,
type: data.type,
...data.attributes,
};

if (Object.prototype.hasOwnProperty.call(data, 'relationships')) {
let includedRecord;
Object.keys(data.relationships).forEach((relationshipName) => {
output[relationshipName] = data.relationships[relationshipName].data;
if (Array.isArray(output[relationshipName])) {
output[relationshipName].forEach((rel, i) => {
includedRecord = findIncluded(included, rel.id, rel.type, mainRecord);
if (includedRecord) {
output[relationshipName][i] = deserializeSingle(includedRecord, otherRows, included, mainRecord);
} else {
includedRecord = findIncluded(otherRows, rel.id, rel.type, mainRecord);
if (includedRecord) {
output[relationshipName][i] = deserializeSingle(includedRecord, otherRows, included, mainRecord);
}
}
});
} else if (output[relationshipName] !== null) {
includedRecord = findIncluded(included, output[relationshipName].id, output[relationshipName].type, mainRecord);
if (includedRecord) {
output[relationshipName] = deserializeSingle(includedRecord, otherRows, included, mainRecord);
} else {
includedRecord = findIncluded(otherRows, output[relationshipName].id, output[relationshipName].type, mainRecord);
if (includedRecord) {
output[relationshipName] = deserializeSingle(includedRecord, otherRows, included, mainRecord);
}
}
}
});

const hasRelationships = Object.keys(record.relationships).length > 0;
if (!hasRelationships) {
delete record.relationships;
}

if (Object.prototype.hasOwnProperty.call(data, 'meta')) {
output.meta = data.meta;
if (!hasAttributes && !hasRelationships) {
return null;
}

return output;
return record;
};

export const deserialize = (body) => {
if (Array.isArray(body.data)) {
const output = [];
body.data.forEach((data) => {
output.push(deserializeSingle(data, body.data, body.included, null));
});

if (Object.prototype.hasOwnProperty.call(body, 'meta')) {
return { data: output, meta: body.meta };
export const filterByKey = (relationshipNames, key) => {
const output = [];
relationshipNames.forEach((relName) => {
if (relName.startsWith(`${key}.`)) {
const keys = relName.split('.');
keys.shift();
output.push(keys.join('.'));
}

return output;
}
return deserializeSingle(body.data, [], body.included, body.data);
};

const cleanSingleRelationship = (values) => ({
id: values.id,
type: values.type,
});

const cleanRelationship = (values) => {
if (Array.isArray(values)) {
return values.map((value) => cleanSingleRelationship(value));
}
return cleanSingleRelationship(values);
});
return output;
};

const getIncludedItemData = (rel, relName, childRelationshipNames, dirtyRelationships, relIndex = null) => {
const relData = {
id: rel.id,
type: rel.type,
attributes: {},
relationships: {},
};

if (rel.id.startsWith('temp-')) {
// This is a new record; include all attributes.
Object.keys(rel).forEach((key) => {
if (key !== 'id' && key !== 'type') {
if (childRelationshipNames[relName].includes(key)) {
const childRel = {
data: {
id: rel[key].id,
type: rel[key].type,
},
};
set(relData.relationships, key, childRel);
} else {
set(relData.attributes, key, rel[key]);
}
}
});
} else {
// This is an existing record; include only the dirty attributes.
Object.keys(rel).forEach((key) => {
if (key !== 'id' && key !== 'type') {
if (relIndex === null) {
if (Object.prototype.hasOwnProperty.call(dirtyRelationships[relName], key)) {
set(relData.attributes, key, rel[key]);
}
} else if (Object.prototype.hasOwnProperty.call(dirtyRelationships[relName], relIndex)) {
if (Object.prototype.hasOwnProperty.call(dirtyRelationships[relName][relIndex], key)) {
set(relData.attributes, key, rel[key]);
export const getDirtyRecords = (record, relationshipNames, dirtyRelationships) => {
let otherRecords = [];
const output = getSimpleRecord(record);
output.attributes = {};
output.relationships = {};

Object.keys(record).forEach((key) => {
if (key !== 'id' && key !== 'type') {
if (Object.prototype.hasOwnProperty.call(dirtyRelationships, key)) {
if (relationshipNames.includes(key)) {
if (Array.isArray(record[key])) {
// This is an array relationship.
const data = [];
record[key].forEach((rel, relIndex) => {
if (typeof dirtyRelationships[key][relIndex] !== 'undefined') {
// eslint-disable-next-line no-use-before-define
const x = getIncludedRecordData(rel, filterByKey(relationshipNames, key), dirtyRelationships[key][relIndex]);
otherRecords = otherRecords.concat(x);
}
data.push(getSimpleRecord(rel));
});
set(output.relationships, key, { data });
} else {
// This is an object relationship.
// eslint-disable-next-line no-use-before-define
const x = getIncludedRecordData(record[key], filterByKey(relationshipNames, key), dirtyRelationships[key]);
const data = x.shift();
otherRecords = otherRecords.concat(x);
set(output.relationships, key, { data });
}
} else {
// This is an attribute.
set(output.attributes, key, record[key]);
}
}
});
}
});

const rec = cleanRecord(output);
if (rec !== null) {
otherRecords.unshift(rec);
}

const hasAttributes = Object.keys(relData.attributes).length > 0;
if (!hasAttributes) {
delete relData.attributes;
return otherRecords;
};

export const getIncludedRecordData = (record, relationshipNames, dirtyRelationships) => {
// This is a new record; include all attributes.
if (record.id.startsWith('temp-')) {
return getDirtyRecords(record, relationshipNames, dirtyRelationships);
}
const hasRelationships = Object.keys(relData.relationships).length > 0;
if (!hasRelationships) {
delete relData.relationships;

// This is an existing record with no changes; don't include it.
if (typeof dirtyRelationships === 'undefined') {
return [];
}

if (!hasAttributes && !hasRelationships) {
return null;
// This is an existing record with no changes, but it's part of a relationship; include only the id/type.
if (Object.keys(dirtyRelationships).length <= 0) {
return [getSimpleRecord(record)];
}

return relData;
// This is an existing record with changes; include all the dirty attributes and relationships.
return getDirtyRecords(record, relationshipNames, dirtyRelationships);
};

const getIncluded = (data, dirtyKeys, relationshipNames) => {
const included = [];

const dirtyRelationships = {};
export const getDirtyRelationships = (dirtyKeys) => {
const output = {};
dirtyKeys.forEach((key) => {
const currentKeys = [];
key.split('.').forEach((k) => {
currentKeys.push(k);
if (typeof get(dirtyRelationships, currentKeys.join('.')) === 'undefined') {
set(dirtyRelationships, currentKeys.join('.'), {});
if (typeof get(output, currentKeys.join('.')) === 'undefined') {
set(output, currentKeys.join('.'), {});
}
});
});
return output;
};

const childRelationshipNames = {};
relationshipNames.forEach((relName) => {
childRelationshipNames[relName] = [];
if (relName.includes('.')) {
const keys = relName.split('.');
childRelationshipNames[keys.shift()].push(keys.join('.'));
}
});
export const getIncludedRecords = (data, dirtyKeys, relationshipNames) => {
let output = [];
if (dirtyKeys.length <= 0) {
return output;
}

const dirtyRelationships = getDirtyRelationships(dirtyKeys);

// For each dirty relationship, add the dirty records to the output.
Object.keys(dirtyRelationships).forEach((relName) => {
if (Object.prototype.hasOwnProperty.call(data.relationships, relName)) {
let relData;
if (Array.isArray(data.relationships[relName].data)) {
// This is an array relationship.
Object.keys(data.relationships[relName].data).forEach((relIndex) => {
const rel = data.relationships[relName].data[relIndex];
if (rel) {
relData = getIncludedItemData(rel, relName, childRelationshipNames, dirtyRelationships, relIndex);
if (relData) {
included.push(relData);
}
const record = data.relationships[relName].data[relIndex];
if (record) {
const records = getIncludedRecordData(record, filterByKey(relationshipNames, relName), dirtyRelationships[relName][relIndex]);
output = output.concat(records);
}
});
} else {
const rel = data.relationships[relName].data;
if (rel) {
relData = getIncludedItemData(rel, relName, childRelationshipNames, dirtyRelationships);
if (relData) {
included.push(relData);
}
// This is an object relationship.
const record = data.relationships[relName].data;
if (record) {
const records = getIncludedRecordData(record, filterByKey(relationshipNames, relName), dirtyRelationships[relName]);
output = output.concat(records);
}
}
}
});

return included;
// Remove records with only an id/type.
return output.filter((record) => (Object.keys(record).length > 2));
};

const unset = (obj, key) => {
export const unset = (obj, key) => {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return delete obj[key];
}
Expand All @@ -216,7 +172,7 @@ const unset = (obj, key) => {
return delete o[lastKey];
};

const appendToFormData = (obj, formData, prefix = '') => {
export const appendToFormData = (obj, formData, prefix = '') => {
Object.entries(obj).forEach((entry) => {
const [key, value] = entry;
const newKey = prefix ? `${prefix}[${key}]` : key;
Expand All @@ -229,15 +185,15 @@ const appendToFormData = (obj, formData, prefix = '') => {
return formData;
};

export const getBody = (
export const getBody = ( // eslint-disable-line import/prefer-default-export
method,
type,
id,
formState,
dirtyKeys,
relationshipNames,
filterBody,
filterValues
filterBody = null,
filterValues = null
) => {
let body = null;

Expand Down Expand Up @@ -290,7 +246,7 @@ export const getBody = (

body = { data };

const included = getIncluded(data, dirtyKeys, relationshipNames);
const included = getIncludedRecords(data, dirtyKeys, relationshipNames);
if (included.length > 0) {
body.included = included;
}
Expand Down
Loading

0 comments on commit 19b0791

Please sign in to comment.