Skip to content

Commit

Permalink
chore: better support for refs and pk-nulls, adds reload option
Browse files Browse the repository at this point in the history
  • Loading branch information
pateketrueke committed Dec 31, 2022
1 parent 379aa1a commit 63e96cd
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 62 deletions.
3 changes: 1 addition & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,11 @@ export interface ResourceAttachment {
}

export interface ResourceOptions {
raw?: boolean;
keys?: string[];
where?: string;
reload?: boolean;
payload?: JsonObject;
logging?: boolean | ((sql: string, timing?: number) => void);
noupdate?: boolean;
fallthrough?: boolean;
attachments?: {
files?: {
Expand Down
168 changes: 108 additions & 60 deletions lib/res.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ function _cleanData(values, model) {
return values;
}

// Sequelize is yielding `row.null` for some reason!
function _fixPkNull(row, model) {
/* istanbul ignore else */
if (row[model.primaryKeyAttribute] === null && row.null > 0) {
row.dataValues[model.primaryKeyAttribute] = row.null;
delete row.null;
}
return row;
}

function _isData(value) {
return typeof value === 'string' && RE_DATA.test(value);
}
Expand Down Expand Up @@ -120,28 +130,27 @@ function _pushUpload(key, payload, metadata, destFile, onUploadCallback) {
}

function _saveBase64(key, payload, attachments, onUploadCallback) {
return Promise.resolve()
.then(() => {
const baseDir = attachments.baseDir || process.cwd();
const details = payload[key].match(RE_DATA)[1].split(';');
const base64Data = payload[key].replace(RE_DATA, '');

const fileName = details[1] ? `_${details[1].split('name=')[1]}` : `_${details[0].replace(/\W+/g, '.')}`;
const destFile = path.join(baseDir, attachments.uploadDir || 'uploads', `${Date.now()}${fileName}`);

// save file on disk before anything else!
fs.outputFileSync(destFile, base64Data, 'base64');

const metadata = {
mtime: new Date(),
path: path.relative(baseDir, destFile),
name: path.basename(destFile),
size: fs.statSync(destFile).size,
type: details[0],
};
const baseDir = attachments.baseDir || process.cwd();
const details = payload[key].match(RE_DATA)[1].split(';');
const base64Data = payload[key].replace(RE_DATA, '');

const fileName = details[1] ? `_${details[1].split('name=')[1]}` : `_${details[0].replace(/\W+/g, '.')}`;
const destFile = path.join(baseDir, attachments.uploadDir || 'uploads', `${Date.now()}${fileName}`);

// save file on disk before anything else!
fs.outputFileSync(destFile, base64Data, 'base64');

const metadata = {
mtime: new Date(),
path: path.relative(baseDir, destFile),
name: path.basename(destFile),
size: fs.statSync(destFile).size,
type: details[0],
};

return _pushUpload(key, {}, metadata, destFile, onUploadCallback);
});
return Promise.resolve()
.then(() => _pushUpload(key, {}, metadata, destFile, onUploadCallback))
.then(result => result || metadata);
}

function _saveUpload(field, payload, attachments, onUploadCallback) {
Expand All @@ -167,6 +176,7 @@ function _walkInput(data, _schema, models, references, walkCallback) {

/* istanbul ignore else */
if (!walkCallback(obj, null, null, rootSchema, parent)) {
const _skip = [];
Object.keys(obj).forEach(key => {
let schema = rootSchema;
if (references[key]) {
Expand All @@ -179,19 +189,28 @@ function _walkInput(data, _schema, models, references, walkCallback) {
}

/* istanbul ignore else */
if (!walkCallback(obj, key, prop, schema, parent)) {
if (!_skip.includes(key) && !walkCallback(obj, key, prop, schema, parent)) {
obj[key] = walk(obj[key], key, obj, schema);
}
} else {
/* istanbul ignore else */
if (references[prop] && models[references[prop].model]) {
if (references[prop] && references[prop].through && obj[references[prop].through]) {
_skip.push(...Object.keys(references[references[prop].model].properties));
_skip.push(references[prop].through);

const ref = obj[references[prop].through];

delete obj[references[prop].through];
obj = { ...ref, [references[prop].model]: obj };
schema = references[references[prop].through];
} else if (references[prop] && models[references[prop].model]) {
schema = models[references[prop].model].options.$schema;
} else if (schema.properties && key !== '$upload') {
schema = schema.properties[key];
}

/* istanbul ignore else */
if (!walkCallback(obj, key, prop, schema, parent)) {
if (!_skip.includes(key) && !walkCallback(obj, key, prop, schema, parent)) {
obj[key] = walk(obj[key], key, obj, schema);
}
}
Expand All @@ -201,6 +220,19 @@ function _walkInput(data, _schema, models, references, walkCallback) {
})(data, null, null, _schema);
}

function _fixedURL(params) {
const parts = ['url:'];

/* istanbul ignore else */
if (params.type) parts.push(params.type, ';');
/* istanbul ignore else */
if (params.size) parts.push(params.size, ',');
/* istanbul ignore else */
if (params.name) parts.push(params.name, '@');

return parts.concat(params.path).join('');
}

function _buildTasks(references, inputData, modelName, _options, models) {
const tasks = {
before: [],
Expand All @@ -211,9 +243,7 @@ function _buildTasks(references, inputData, modelName, _options, models) {
_walkInput(inputData, models[modelName].options.$schema, models, references, (obj, key, prop, schema, parent) => {
/* istanbul ignore else */
if (key === null) {
tasks.before.push(() => {
tasks.rows.push([obj, Object.keys(obj)]);
});
tasks.before.push(() => tasks.rows.push(obj));
}

/* istanbul ignore else */
Expand All @@ -227,16 +257,7 @@ function _buildTasks(references, inputData, modelName, _options, models) {
} else if (schema.type === 'object') {
parent[prop] = result;
} else if (schema.type === 'string') {
const parts = ['url:'];

/* istanbul ignore else */
if (result.type) parts.push(result.type, ';');
/* istanbul ignore else */
if (result.size) parts.push(result.size, ',');
/* istanbul ignore else */
if (result.name) parts.push(result.name, '@');

parent[prop] = parts.concat(result.path).join('');
parent[prop] = _fixedURL(result);
} else {
parent[prop] = result;
}
Expand All @@ -247,25 +268,26 @@ function _buildTasks(references, inputData, modelName, _options, models) {
/* istanbul ignore else */
if (_isData(obj[key])) {
tasks.before.unshift(() => _saveBase64(key, obj, _options.attachments, _options.upload).then(result => {
obj[key] = result;
obj[key] = schema.type === 'string' ? _fixedURL(result) : result;
}));
return true;
}
});

tasks.after.push(async (fk, opts, isUpdate) => {
tasks.after.push(async (fk, opts, cursor, isUpdate) => {
const defaults = { logging: opts.logging, transaction: opts.transaction };

function update(model, input, where, key, pk) {
return model.update({ ...input, [key]: undefined }, { ...defaults, where: { ...where, [key]: pk } });
}

async function upsert(key, model, [context, properties]) {
async function upsert(key, model, context) {
const props = Object.keys(context);
const refs = model.associations;

for (const field of properties) {
for (const field of props) {
/* istanbul ignore else */
if (refs[field]) {
if (refs[field] && context[field]) {
const input = context[field];
const {
target, through, associationType, targetKey, foreignKey, foreignIdentifier,
Expand All @@ -276,9 +298,14 @@ function _buildTasks(references, inputData, modelName, _options, models) {
for (const item of input) {
const other = (through && through.model) || model;

await upsert(key, other, tasks.rows.shift());
await upsert(key, target, tasks.rows.shift());

if (isUpdate) {
/* istanbul ignore else */
if (through) {
await upsert(key, other, item);
}

if (isUpdate && (item[targetKey] || item[foreignIdentifier])) {
const where = {
[foreignKey]: key,
};
Expand All @@ -291,11 +318,12 @@ function _buildTasks(references, inputData, modelName, _options, models) {
}

delete item[targetKey];
await other.update(item, { ...defaults, where });
const [row, created] = await other.findOrCreate({ ...defaults, where, defaults: item });
if (!created) await row.update(item);
} else {
item[foreignKey] = key;
const row = await other.create(item, defaults);
item[targetKey] = row[targetKey];
item[targetKey] = _fixPkNull(row, other)[targetKey];
}
}
}
Expand All @@ -307,20 +335,29 @@ function _buildTasks(references, inputData, modelName, _options, models) {
await update(target, input, null, targetKey, pk = input[targetKey]);
} else {
const row = await target.create(input, defaults);
pk = input[targetKey] = row[targetKey];
pk = input[targetKey] = _fixPkNull(row, target)[targetKey];
}

/* istanbul ignore else */
if (cursor && cursor.rawAttributes[foreignKey]) {
await cursor.update({ [foreignKey]: pk });
}

context[foreignKey] = pk;
delete context[field];
await upsert(pk, target, tasks.rows.shift());

/* istanbul ignore else */
if (tasks.rows.length > 0) {
await upsert(pk, target, tasks.rows.shift());
}
}

/* istanbul ignore else */
if (associationType === 'HasMany') {
const pk = target.primaryKeyAttribute;

for (const item of input) {
if (item[pk]) {
if (isUpdate && item[pk]) {
await update(target, item, null, pk, item[pk]);
} else {
item[foreignKey] = key;
Expand All @@ -338,7 +375,7 @@ function _buildTasks(references, inputData, modelName, _options, models) {
row = context[field] = input[0];
}

if (row[pk]) {
if (isUpdate && row[pk]) {
await update(target, row, null, pk, row[pk]);
} else {
context[foreignKey] = row[foreignKey] = key;
Expand Down Expand Up @@ -700,41 +737,52 @@ module.exports = (conn, options, modelName) => {
function build(payload, isUpdate, _options) {
_options = _options || {};
_options.logging = options.logging || _options.logging;
_options.reload = options.reload || _options.reload;
_options.where = Object.assign({}, _options.where, _where);

const _pk = model.primaryKeyAttribute;

let _payload;
let _tasks;
let _opts;

return Promise.resolve()
.then(() => {
_payload = _cleanData({ ...(payload || options.payload) }, model);
_tasks = _buildTasks(_props, _payload, model.name, options, conn.models);
_opts = _getOpts(model, _props, isUpdate ? 'update' : 'create', _options);

delete _payload[_pk];
})
.then(() => _tasks.before.reduce((prev, cur) => prev.then(() => cur(_opts, isUpdate)), Promise.resolve()))
.then(() => model[isUpdate ? 'update' : 'create'](_payload, _opts))
.then(row => {
let pk;

/* istanbul ignore else */
if (!Array.isArray(row)) {
pk = row[model.primaryKeyAttribute];
pk = _fixPkNull(row, model)[_pk];
}

/* istanbul ignore else */
if (!pk && _opts.where) {
pk = _opts.where[model.primaryKeyAttribute];
pk = _opts.where[_pk];
}

_payload[model.primaryKeyAttribute] = _payload[model.primaryKeyAttribute] || pk;
_payload[_pk] = _payload[_pk] || pk;

/* istanbul ignore else */
if (!isUpdate) {
row = [row, _payload];
}
return Promise.resolve()
.then(() => (!isUpdate ? [row, _payload] : row))
.then(result => _tasks.after.reduce((prev, cur) => prev
.then(() => cur(pk, _opts, !isUpdate ? result[0] : null, isUpdate)), Promise.resolve())
.then(() => {
/* istanbul ignore else */
if (_opts.reload) {
_opts = _getOpts(model, _props, 'findOne', _options);
_opts.where[_pk] = pk;

return _tasks.after.reduce((prev, cur) => prev.then(() => cur(pk, _opts, isUpdate)), Promise.resolve()).then(() => row);
return model.findOne(_opts).then(x => (!isUpdate ? [x, _payload] : x));
}
return result;
}));
})
.catch(err);
}
Expand Down

0 comments on commit 63e96cd

Please sign in to comment.