From 63e96cdda9d8a4a87febd23878fec274f8c73c62 Mon Sep 17 00:00:00 2001 From: Alvaro Cabrera Date: Sat, 31 Dec 2022 03:09:09 -0600 Subject: [PATCH] chore: better support for refs and pk-nulls, adds reload option --- index.d.ts | 3 +- lib/res.js | 168 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 109 insertions(+), 62 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5878f76..95bfe8f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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?: { diff --git a/lib/res.js b/lib/res.js index bc8529d..03699ad 100644 --- a/lib/res.js +++ b/lib/res.js @@ -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); } @@ -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) { @@ -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]) { @@ -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); } } @@ -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: [], @@ -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 */ @@ -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; } @@ -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, @@ -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, }; @@ -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]; } } } @@ -307,12 +335,21 @@ 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 */ @@ -320,7 +357,7 @@ function _buildTasks(references, inputData, modelName, _options, models) { 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; @@ -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; @@ -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); }