Standartized service/DB API contracts
This commit is contained in:
parent
27170d90ec
commit
cf3deb14f7
95
backend/src/contracts/entity-options.js
Normal file
95
backend/src/contracts/entity-options.js
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @typedef {Object} ServiceCreateOptions
|
||||
* @property {Object} data
|
||||
* @property {Object} [currentUser]
|
||||
* @property {Object} [transaction]
|
||||
* @property {Object} [runtimeContext]
|
||||
* @property {boolean} [sendInvitationEmails]
|
||||
* @property {string} [host]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EntityIdOptions
|
||||
* @property {string} id
|
||||
* @property {Object} [currentUser]
|
||||
* @property {Object} [transaction]
|
||||
* @property {Object} [runtimeContext]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DeleteByIdsOptions
|
||||
* @property {string[]} ids
|
||||
* @property {Object} [currentUser]
|
||||
* @property {Object} [transaction]
|
||||
* @property {Object} [runtimeContext]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AutocompleteOptions
|
||||
* @property {string} [query]
|
||||
* @property {number} [limit]
|
||||
* @property {number} [offset]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} UpdateOptions
|
||||
* @property {string} id
|
||||
* @property {Object} data
|
||||
* @property {Object} [currentUser]
|
||||
* @property {Object} [transaction]
|
||||
* @property {Object} [runtimeContext]
|
||||
*/
|
||||
|
||||
function assertOptionsObject(options, contractName, methodName) {
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||
throw new TypeError(
|
||||
`${contractName}.${methodName} expects an options object`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertCreateOptions(options, contractName) {
|
||||
assertOptionsObject(options, contractName, 'create');
|
||||
|
||||
if (options.data === undefined) {
|
||||
throw new TypeError(`${contractName}.create requires { data }`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertIdOptions(options, contractName, methodName) {
|
||||
assertOptionsObject(options, contractName, methodName);
|
||||
|
||||
if (!options.id) {
|
||||
throw new TypeError(`${contractName}.${methodName} requires { id }`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertDeleteByIdsOptions(options, contractName) {
|
||||
assertOptionsObject(options, contractName, 'deleteByIds');
|
||||
|
||||
if (!Array.isArray(options.ids)) {
|
||||
throw new TypeError(`${contractName}.deleteByIds requires { ids }`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertAutocompleteOptions(options, contractName) {
|
||||
assertOptionsObject(options, contractName, 'findAllAutocomplete');
|
||||
}
|
||||
|
||||
function assertUpdateOptions(options, contractName) {
|
||||
assertOptionsObject(options, contractName, 'update');
|
||||
|
||||
if (!options.id || options.data === undefined) {
|
||||
throw new TypeError(
|
||||
`${contractName}.update requires { id, data } in the options object`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
assertAutocompleteOptions,
|
||||
assertCreateOptions,
|
||||
assertDeleteByIdsOptions,
|
||||
assertIdOptions,
|
||||
assertUpdateOptions,
|
||||
};
|
||||
@ -2,6 +2,13 @@ const db = require('../models');
|
||||
const Utils = require('../utils');
|
||||
const { parse } = require('json2csv');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const {
|
||||
assertAutocompleteOptions,
|
||||
assertCreateOptions,
|
||||
assertDeleteByIdsOptions,
|
||||
assertIdOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../../contracts/entity-options');
|
||||
|
||||
const Sequelize = db.Sequelize;
|
||||
const Op = Sequelize.Op;
|
||||
@ -147,9 +154,10 @@ class GenericDBApi {
|
||||
return mapped;
|
||||
}
|
||||
|
||||
static async create(data, options = {}) {
|
||||
const currentUser = options.currentUser || { id: null };
|
||||
const transaction = options.transaction;
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'DBApi');
|
||||
|
||||
const { data, currentUser = { id: null }, transaction } = options;
|
||||
|
||||
const mappedData = this.getFieldMapping(data);
|
||||
|
||||
@ -190,9 +198,17 @@ class GenericDBApi {
|
||||
return this.MODEL.bulkCreate(recordsData, { transaction });
|
||||
}
|
||||
|
||||
static async update(id, data, options = {}) {
|
||||
const currentUser = options.currentUser || { id: null };
|
||||
const transaction = options.transaction;
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {string} options.id
|
||||
* @param {Object} options.data
|
||||
* @param {Object} [options.currentUser]
|
||||
* @param {Object} [options.transaction]
|
||||
*/
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'DBApi');
|
||||
|
||||
const { id, data, currentUser = { id: null }, transaction } = options;
|
||||
|
||||
const record = await this.MODEL.findByPk(id, { transaction });
|
||||
|
||||
@ -227,13 +243,16 @@ class GenericDBApi {
|
||||
*
|
||||
* Use this when you need to update specific fields without affecting others.
|
||||
*
|
||||
* @param {string} id - Record ID
|
||||
* @param {Object} data - Fields to update (only these will be modified)
|
||||
* @param {Object} options - Options with currentUser and transaction
|
||||
* @param {Object} options
|
||||
* @param {string} options.id - Record ID
|
||||
* @param {Object} options.data - Fields to update
|
||||
* @param {Object} [options.currentUser]
|
||||
* @param {Object} [options.transaction]
|
||||
*/
|
||||
static async partialUpdate(id, data, options = {}) {
|
||||
const currentUser = options.currentUser || { id: null };
|
||||
const transaction = options.transaction;
|
||||
static async partialUpdate(options) {
|
||||
assertUpdateOptions(options, 'DBApi');
|
||||
|
||||
const { id, data, currentUser = { id: null }, transaction } = options;
|
||||
|
||||
const record = await this.MODEL.findByPk(id, { transaction });
|
||||
|
||||
@ -255,9 +274,10 @@ class GenericDBApi {
|
||||
return record;
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, options = {}) {
|
||||
const currentUser = options.currentUser || { id: null };
|
||||
const transaction = options.transaction;
|
||||
static async deleteByIds(options) {
|
||||
assertDeleteByIdsOptions(options, 'DBApi');
|
||||
|
||||
const { ids, currentUser = { id: null }, transaction } = options;
|
||||
|
||||
const records = await this.MODEL.findAll({
|
||||
where: { id: { [Op.in]: ids } },
|
||||
@ -274,9 +294,10 @@ class GenericDBApi {
|
||||
return records;
|
||||
}
|
||||
|
||||
static async remove(id, options = {}) {
|
||||
const currentUser = options.currentUser || { id: null };
|
||||
const transaction = options.transaction;
|
||||
static async remove(options) {
|
||||
assertIdOptions(options, 'DBApi', 'remove');
|
||||
|
||||
const { id, currentUser = { id: null }, transaction } = options;
|
||||
|
||||
const record = await this.MODEL.findByPk(id, { transaction });
|
||||
|
||||
@ -453,7 +474,11 @@ class GenericDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset) {
|
||||
static async findAllAutocomplete(options, queryOptions = {}) {
|
||||
assertAutocompleteOptions(options, 'DBApi');
|
||||
|
||||
const { query, limit, offset } = options;
|
||||
const transaction = queryOptions.transaction;
|
||||
let where = {};
|
||||
|
||||
if (query) {
|
||||
@ -474,6 +499,7 @@ class GenericDBApi {
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
offset: offset ? Number(offset) : undefined,
|
||||
order: [[this.AUTOCOMPLETE_FIELD, 'ASC']],
|
||||
transaction,
|
||||
});
|
||||
|
||||
return records.map((record) => ({
|
||||
|
||||
@ -363,9 +363,9 @@ class Element_type_defaultsDBApi extends GenericDBApi {
|
||||
await this.initializationPromise;
|
||||
}
|
||||
|
||||
static async create(data, options = {}) {
|
||||
static async create(options) {
|
||||
await this.ensureInitialized();
|
||||
return super.create(data, options);
|
||||
return super.create(options);
|
||||
}
|
||||
|
||||
static async bulkImport(data, options = {}) {
|
||||
@ -373,19 +373,19 @@ class Element_type_defaultsDBApi extends GenericDBApi {
|
||||
return super.bulkImport(data, options);
|
||||
}
|
||||
|
||||
static async update(id, data, options = {}) {
|
||||
static async update({ id, data, currentUser, transaction, runtimeContext }) {
|
||||
await this.ensureInitialized();
|
||||
return super.update(id, data, options);
|
||||
return super.update({ id, data, currentUser, transaction, runtimeContext });
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, options = {}) {
|
||||
static async deleteByIds(options) {
|
||||
await this.ensureInitialized();
|
||||
return super.deleteByIds(ids, options);
|
||||
return super.deleteByIds(options);
|
||||
}
|
||||
|
||||
static async remove(id, options = {}) {
|
||||
static async remove(options) {
|
||||
await this.ensureInitialized();
|
||||
return super.remove(id, options);
|
||||
return super.remove(options);
|
||||
}
|
||||
|
||||
static async findBy(where, options = {}) {
|
||||
@ -398,9 +398,9 @@ class Element_type_defaultsDBApi extends GenericDBApi {
|
||||
return super.findAll(filter, options);
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset) {
|
||||
static async findAllAutocomplete(options, queryOptions = {}) {
|
||||
await this.ensureInitialized();
|
||||
return super.findAllAutocomplete(query, limit, offset);
|
||||
return super.findAllAutocomplete(options, queryOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -134,9 +134,9 @@ class Global_transition_defaultsDBApi extends GenericDBApi {
|
||||
return this.findOne(options);
|
||||
}
|
||||
|
||||
static async update(id, data, options = {}) {
|
||||
static async update({ id, data, currentUser, transaction, runtimeContext }) {
|
||||
await this.ensureInitialized();
|
||||
return super.update(id, data, options);
|
||||
return super.update({ id, data, currentUser, transaction, runtimeContext });
|
||||
}
|
||||
|
||||
static async findBy(where, options = {}) {
|
||||
|
||||
@ -148,9 +148,9 @@ class Global_ui_control_defaultsDBApi extends GenericDBApi {
|
||||
return record.get({ plain: true });
|
||||
}
|
||||
|
||||
static async update(id, data, options = {}) {
|
||||
static async update({ id, data, currentUser, transaction, runtimeContext }) {
|
||||
await this.ensureInitialized();
|
||||
return super.update(id, data, options);
|
||||
return super.update({ id, data, currentUser, transaction, runtimeContext });
|
||||
}
|
||||
|
||||
static async findBy(where, options = {}) {
|
||||
|
||||
@ -115,11 +115,11 @@ class ProjectsDBApi extends GenericDBApi {
|
||||
/**
|
||||
* Create a new project and auto-snapshot global element defaults
|
||||
*/
|
||||
static async create(data, options = {}) {
|
||||
const transaction = options.transaction;
|
||||
static async create(options) {
|
||||
const { transaction } = options;
|
||||
|
||||
// Create the project using parent's create
|
||||
const project = await super.create(data, options);
|
||||
const project = await super.create(options);
|
||||
|
||||
// Auto-snapshot global element defaults to the new project
|
||||
// Errors propagate to service layer → transaction rollback → proper error to client
|
||||
|
||||
@ -124,9 +124,8 @@ class Tour_pagesDBApi extends GenericDBApi {
|
||||
};
|
||||
}
|
||||
|
||||
static async create(data, options = {}) {
|
||||
const currentUser = options.currentUser || { id: null };
|
||||
const transaction = options.transaction;
|
||||
static async create(options) {
|
||||
const { data, currentUser = { id: null }, transaction } = options;
|
||||
const projectId = data.project || data.projectId || null;
|
||||
|
||||
const record = await this.MODEL.create(
|
||||
|
||||
@ -6,6 +6,13 @@ const Utils = require('../utils');
|
||||
const bcrypt = require('bcrypt');
|
||||
const config = require('../../config');
|
||||
const { logger } = require('../../utils/logger');
|
||||
const {
|
||||
assertAutocompleteOptions,
|
||||
assertCreateOptions,
|
||||
assertDeleteByIdsOptions,
|
||||
assertIdOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../../contracts/entity-options');
|
||||
|
||||
const Sequelize = db.Sequelize;
|
||||
const Op = Sequelize.Op;
|
||||
@ -60,40 +67,42 @@ module.exports = class UsersDBApi {
|
||||
];
|
||||
}
|
||||
|
||||
static async create(data, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'DBApi');
|
||||
|
||||
const { data, currentUser = { id: null }, transaction } = options;
|
||||
const userData = data.data || data;
|
||||
const password =
|
||||
data.data.password || crypto.randomBytes(20).toString('hex');
|
||||
userData.password || crypto.randomBytes(20).toString('hex');
|
||||
|
||||
const users = await db.users.create(
|
||||
{
|
||||
id: data.data.id || undefined,
|
||||
id: userData.id || undefined,
|
||||
|
||||
firstName: data.data.firstName || null,
|
||||
lastName: data.data.lastName || null,
|
||||
phoneNumber: data.data.phoneNumber || null,
|
||||
email: data.data.email || null,
|
||||
disabled: data.data.disabled || false,
|
||||
firstName: userData.firstName || null,
|
||||
lastName: userData.lastName || null,
|
||||
phoneNumber: userData.phoneNumber || null,
|
||||
email: userData.email || null,
|
||||
disabled: userData.disabled || false,
|
||||
|
||||
password: bcrypt.hashSync(password, config.bcrypt.saltRounds),
|
||||
emailVerified: data.data.emailVerified || true,
|
||||
emailVerified: userData.emailVerified || true,
|
||||
|
||||
emailVerificationToken: data.data.emailVerificationToken || null,
|
||||
emailVerificationToken: userData.emailVerificationToken || null,
|
||||
emailVerificationTokenExpiresAt:
|
||||
data.data.emailVerificationTokenExpiresAt || null,
|
||||
passwordResetToken: data.data.passwordResetToken || null,
|
||||
userData.emailVerificationTokenExpiresAt || null,
|
||||
passwordResetToken: userData.passwordResetToken || null,
|
||||
passwordResetTokenExpiresAt:
|
||||
data.data.passwordResetTokenExpiresAt || null,
|
||||
provider: data.data.provider || config.providers.LOCAL,
|
||||
importHash: data.data.importHash || null,
|
||||
userData.passwordResetTokenExpiresAt || null,
|
||||
provider: userData.provider || config.providers.LOCAL,
|
||||
importHash: userData.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
if (!data.data.app_role) {
|
||||
if (!userData.app_role) {
|
||||
const role = await db.roles.findOne({
|
||||
where: { name: 'User' },
|
||||
});
|
||||
@ -103,12 +112,12 @@ module.exports = class UsersDBApi {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await users.setApp_role(data.data.app_role || null, {
|
||||
await users.setApp_role(userData.app_role || null, {
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
await users.setCustom_permissions(data.data.custom_permissions || [], {
|
||||
await users.setCustom_permissions(userData.custom_permissions || [], {
|
||||
transaction,
|
||||
});
|
||||
|
||||
@ -118,7 +127,7 @@ module.exports = class UsersDBApi {
|
||||
belongsToColumn: 'avatar',
|
||||
belongsToId: users.id,
|
||||
},
|
||||
data.data.avatar,
|
||||
userData.avatar,
|
||||
options,
|
||||
);
|
||||
|
||||
@ -177,9 +186,16 @@ module.exports = class UsersDBApi {
|
||||
return users;
|
||||
}
|
||||
|
||||
static async update(id, data, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
static async update(updateOptions) {
|
||||
assertUpdateOptions(updateOptions, 'DBApi');
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
currentUser = { id: null },
|
||||
transaction,
|
||||
runtimeContext,
|
||||
} = updateOptions;
|
||||
const dbOptions = { currentUser, transaction, runtimeContext };
|
||||
|
||||
const users = await db.users.findByPk(id, { transaction });
|
||||
|
||||
@ -272,15 +288,15 @@ module.exports = class UsersDBApi {
|
||||
belongsToId: users.id,
|
||||
},
|
||||
data.avatar,
|
||||
options,
|
||||
dbOptions,
|
||||
);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
static async deleteByIds(options) {
|
||||
assertDeleteByIdsOptions(options, 'DBApi');
|
||||
const { ids, currentUser = { id: null }, transaction } = options;
|
||||
|
||||
const users = await db.users.findAll({
|
||||
where: {
|
||||
@ -291,23 +307,21 @@ module.exports = class UsersDBApi {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await db.sequelize.transaction(async (transaction) => {
|
||||
for (const record of users) {
|
||||
await record.update({ deletedBy: currentUser.id }, { transaction });
|
||||
}
|
||||
for (const record of users) {
|
||||
await record.destroy({ transaction });
|
||||
}
|
||||
});
|
||||
for (const record of users) {
|
||||
await record.update({ deletedBy: currentUser.id }, { transaction });
|
||||
}
|
||||
for (const record of users) {
|
||||
await record.destroy({ transaction });
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
static async remove(id, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
static async remove(options) {
|
||||
assertIdOptions(options, 'DBApi', 'remove');
|
||||
const { id, currentUser = { id: null }, transaction } = options;
|
||||
|
||||
const users = await db.users.findByPk(id, options);
|
||||
const users = await db.users.findByPk(id, { transaction });
|
||||
|
||||
await users.update(
|
||||
{
|
||||
@ -694,7 +708,10 @@ module.exports = class UsersDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset) {
|
||||
static async findAllAutocomplete(options, queryOptions = {}) {
|
||||
assertAutocompleteOptions(options, 'DBApi');
|
||||
const { query, limit, offset } = options;
|
||||
const transaction = queryOptions.transaction;
|
||||
let where = {};
|
||||
|
||||
if (query) {
|
||||
@ -713,6 +730,7 @@ module.exports = class UsersDBApi {
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
offset: offset ? Number(offset) : undefined,
|
||||
orderBy: [['firstName', 'ASC']],
|
||||
transaction,
|
||||
});
|
||||
|
||||
return records.map((record) => ({
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const ROLE_PERMISSIONS = [
|
||||
['Administrator', 'CREATE_PAGE_ELEMENTS'],
|
||||
['Administrator', 'READ_PAGE_ELEMENTS'],
|
||||
['Administrator', 'UPDATE_PAGE_ELEMENTS'],
|
||||
['Administrator', 'DELETE_PAGE_ELEMENTS'],
|
||||
['Platform Owner', 'CREATE_PAGE_ELEMENTS'],
|
||||
['Platform Owner', 'READ_PAGE_ELEMENTS'],
|
||||
['Platform Owner', 'UPDATE_PAGE_ELEMENTS'],
|
||||
['Platform Owner', 'DELETE_PAGE_ELEMENTS'],
|
||||
['Account Manager', 'CREATE_PAGE_ELEMENTS'],
|
||||
['Account Manager', 'READ_PAGE_ELEMENTS'],
|
||||
['Account Manager', 'UPDATE_PAGE_ELEMENTS'],
|
||||
['Tour Designer', 'CREATE_PAGE_ELEMENTS'],
|
||||
['Tour Designer', 'READ_PAGE_ELEMENTS'],
|
||||
['Tour Designer', 'UPDATE_PAGE_ELEMENTS'],
|
||||
['Content Reviewer', 'READ_PAGE_ELEMENTS'],
|
||||
['Content Reviewer', 'UPDATE_PAGE_ELEMENTS'],
|
||||
['Analytics Viewer', 'READ_PAGE_ELEMENTS'],
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.query(
|
||||
`
|
||||
WITH expected(role_name, permission_name) AS (
|
||||
VALUES ${ROLE_PERMISSIONS.map(() => '(?, ?)').join(', ')}
|
||||
)
|
||||
INSERT INTO "rolesPermissionsPermissions"
|
||||
("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
|
||||
SELECT NOW(), NOW(), r.id, p.id
|
||||
FROM expected e
|
||||
JOIN roles r ON r.name = e.role_name
|
||||
JOIN permissions p ON p.name = e.permission_name
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "rolesPermissionsPermissions" rp
|
||||
WHERE rp."roles_permissionsId" = r.id
|
||||
AND rp."permissionId" = p.id
|
||||
)
|
||||
`,
|
||||
{
|
||||
replacements: ROLE_PERMISSIONS.flat(),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(
|
||||
`
|
||||
WITH expected(role_name, permission_name) AS (
|
||||
VALUES ${ROLE_PERMISSIONS.map(() => '(?, ?)').join(', ')}
|
||||
)
|
||||
DELETE FROM "rolesPermissionsPermissions" rp
|
||||
USING expected e, roles r, permissions p
|
||||
WHERE rp."roles_permissionsId" = r.id
|
||||
AND rp."permissionId" = p.id
|
||||
AND r.name = e.role_name
|
||||
AND p.name = e.permission_name
|
||||
`,
|
||||
{
|
||||
replacements: ROLE_PERMISSIONS.flat(),
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -68,12 +68,13 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
||||
req.headers.referer ||
|
||||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
const payload = await Service.create(
|
||||
req.body.data,
|
||||
req.currentUser,
|
||||
true,
|
||||
link.origin,
|
||||
);
|
||||
const payload = await Service.create({
|
||||
data: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
sendInvitationEmails: true,
|
||||
host: link.origin,
|
||||
});
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
@ -95,7 +96,12 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
||||
validateRequest(schemaFor('update')),
|
||||
wrapAsync(async (req, res) => {
|
||||
assertRouteIdMatchesBody(req);
|
||||
await Service.update(req.body.data, req.params.id, req.currentUser);
|
||||
await Service.update({
|
||||
id: req.params.id,
|
||||
data: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
@ -104,7 +110,11 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
||||
'/:id',
|
||||
validateRequest(schemaFor('remove')),
|
||||
wrapAsync(async (req, res) => {
|
||||
await Service.remove(req.params.id, req.currentUser);
|
||||
await Service.remove({
|
||||
id: req.params.id,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
@ -113,7 +123,11 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
||||
'/deleteByIds',
|
||||
validateRequest(schemaFor('deleteByIds')),
|
||||
wrapAsync(async (req, res) => {
|
||||
await Service.deleteByIds(req.body.data, req.currentUser);
|
||||
await Service.deleteByIds({
|
||||
ids: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
@ -174,11 +188,11 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
||||
defaultLimit: 20,
|
||||
maxLimit: MAX_AUTOCOMPLETE_LIMIT,
|
||||
});
|
||||
const payload = await DBApi.findAllAutocomplete(
|
||||
req.query.query,
|
||||
const payload = await DBApi.findAllAutocomplete({
|
||||
query: req.query.query,
|
||||
limit,
|
||||
req.query.offset,
|
||||
);
|
||||
offset: req.query.offset,
|
||||
});
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
@ -3,19 +3,41 @@ const processFile = require('../middlewares/upload');
|
||||
const ValidationError = require('../services/notifications/errors/validation');
|
||||
const csv = require('csv-parser');
|
||||
const stream = require('stream');
|
||||
const {
|
||||
assertCreateOptions,
|
||||
assertDeleteByIdsOptions,
|
||||
assertIdOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../contracts/entity-options');
|
||||
|
||||
function createEntityService(DBApi, options = {}) {
|
||||
const entityName = options.entityName || 'Entity';
|
||||
|
||||
return class GenericService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'Service');
|
||||
|
||||
const {
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
const record = await DBApi.create(data, { currentUser, transaction });
|
||||
await transaction.commit();
|
||||
const record = await DBApi.create({
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return record;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -52,45 +74,103 @@ function createEntityService(DBApi, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {string} options.id
|
||||
* @param {Object} options.data
|
||||
* @param {Object} [options.currentUser]
|
||||
* @param {Object} [options.transaction]
|
||||
* @param {Object} [options.runtimeContext]
|
||||
*/
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'Service');
|
||||
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
const record = await DBApi.findBy({ id }, { transaction });
|
||||
const record = await DBApi.findBy(
|
||||
{ id },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw new ValidationError(`${entityName}NotFound`);
|
||||
}
|
||||
|
||||
const updated = await DBApi.update(id, data, {
|
||||
const updated = await DBApi.update({
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return updated;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async deleteByIds(options) {
|
||||
assertDeleteByIdsOptions(options, 'Service');
|
||||
|
||||
const {
|
||||
ids,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
await DBApi.deleteByIds(ids, { currentUser, transaction });
|
||||
await transaction.commit();
|
||||
await DBApi.deleteByIds({
|
||||
ids,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async remove(options) {
|
||||
assertIdOptions(options, 'Service', 'remove');
|
||||
|
||||
const {
|
||||
id,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
await DBApi.remove(id, { currentUser, transaction });
|
||||
await transaction.commit();
|
||||
await DBApi.remove({
|
||||
id,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,11 +123,12 @@ router.put(
|
||||
jwtAuth,
|
||||
wrapAsync(async (req, res) => {
|
||||
assertRouteIdMatchesBody(req);
|
||||
await Global_transition_defaultsService.update(
|
||||
req.body.data,
|
||||
req.params.id,
|
||||
req.currentUser,
|
||||
);
|
||||
await Global_transition_defaultsService.update({
|
||||
id: req.params.id,
|
||||
data: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
|
||||
@ -44,11 +44,12 @@ router.put(
|
||||
'/:id',
|
||||
jwtAuth,
|
||||
wrapAsync(async (req, res) => {
|
||||
await Global_ui_control_defaultsService.update(
|
||||
req.body.data,
|
||||
req.params.id,
|
||||
req.currentUser,
|
||||
);
|
||||
await Global_ui_control_defaultsService.update({
|
||||
id: req.params.id,
|
||||
data: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
const payload = await Global_ui_control_defaultsDBApi.findOne();
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
|
||||
@ -284,10 +284,11 @@ router.delete(
|
||||
);
|
||||
|
||||
if (settings) {
|
||||
await Project_transition_settingsService.remove(
|
||||
settings.id,
|
||||
req.currentUser,
|
||||
);
|
||||
await Project_transition_settingsService.remove({
|
||||
id: settings.id,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({ success: true });
|
||||
@ -340,10 +341,11 @@ router.post(
|
||||
'/',
|
||||
jwtAuth,
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = await Project_transition_settingsService.create(
|
||||
req.body.data,
|
||||
req.currentUser,
|
||||
);
|
||||
const payload = await Project_transition_settingsService.create({
|
||||
data: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
@ -415,11 +417,12 @@ router.put(
|
||||
jwtAuth,
|
||||
wrapAsync(async (req, res) => {
|
||||
assertRouteIdMatchesBody(req);
|
||||
await Project_transition_settingsService.update(
|
||||
req.body.data,
|
||||
req.params.id,
|
||||
req.currentUser,
|
||||
);
|
||||
await Project_transition_settingsService.update({
|
||||
id: req.params.id,
|
||||
data: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
@ -447,10 +450,11 @@ router.delete(
|
||||
'/:id',
|
||||
jwtAuth,
|
||||
wrapAsync(async (req, res) => {
|
||||
await Project_transition_settingsService.remove(
|
||||
req.params.id,
|
||||
req.currentUser,
|
||||
);
|
||||
await Project_transition_settingsService.remove({
|
||||
id: req.params.id,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
|
||||
@ -161,10 +161,11 @@ router.delete(
|
||||
);
|
||||
|
||||
if (settings) {
|
||||
await Project_ui_control_settingsService.remove(
|
||||
settings.id,
|
||||
req.currentUser,
|
||||
);
|
||||
await Project_ui_control_settingsService.remove({
|
||||
id: settings.id,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({ success: true });
|
||||
|
||||
@ -175,12 +175,13 @@ router.post(
|
||||
req.headers.referer ||
|
||||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
const payload = await Tour_pagesService.create(
|
||||
req.body.data,
|
||||
req.currentUser,
|
||||
true,
|
||||
link.origin,
|
||||
);
|
||||
const payload = await Tour_pagesService.create({
|
||||
data: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
sendInvitationEmails: true,
|
||||
host: link.origin,
|
||||
});
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
@ -231,11 +232,12 @@ router.put(
|
||||
validateRequest(tourPageSchemas.update),
|
||||
wrapAsync(async (req, res) => {
|
||||
assertRouteIdMatchesBody(req);
|
||||
await Tour_pagesService.update(
|
||||
req.body.data,
|
||||
req.params.id,
|
||||
req.currentUser,
|
||||
);
|
||||
await Tour_pagesService.update({
|
||||
id: req.params.id,
|
||||
data: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
@ -245,7 +247,11 @@ router.delete(
|
||||
'/:id',
|
||||
validateRequest(crudSchemas.remove),
|
||||
wrapAsync(async (req, res) => {
|
||||
await Tour_pagesService.remove(req.params.id, req.currentUser);
|
||||
await Tour_pagesService.remove({
|
||||
id: req.params.id,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
@ -255,7 +261,11 @@ router.post(
|
||||
'/deleteByIds',
|
||||
validateRequest(crudSchemas.deleteByIds),
|
||||
wrapAsync(async (req, res) => {
|
||||
await Tour_pagesService.deleteByIds(req.body.data, req.currentUser);
|
||||
await Tour_pagesService.deleteByIds({
|
||||
ids: req.body.data,
|
||||
currentUser: req.currentUser,
|
||||
runtimeContext: req.runtimeContext,
|
||||
});
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
@ -334,11 +344,11 @@ router.get(
|
||||
'/autocomplete',
|
||||
validateRequest(crudSchemas.autocomplete),
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = await Tour_pagesDBApi.findAllAutocomplete(
|
||||
req.query.query,
|
||||
req.query.limit,
|
||||
req.query.offset,
|
||||
);
|
||||
const payload = await Tour_pagesDBApi.findAllAutocomplete({
|
||||
query: req.query.query,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
});
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
const AssetsDBApi = require('../db/api/assets');
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
const {
|
||||
assertCreateOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../contracts/entity-options');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const { downloadToTempFile } = require('./file');
|
||||
const { probeMediaMetadata } = require('./videoProcessing');
|
||||
@ -196,7 +200,11 @@ class AssetsService extends BaseService {
|
||||
/**
|
||||
* Create asset with MIME type validation, embed URL validation, and video pre-processing
|
||||
*/
|
||||
static async create(data, currentUser) {
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'Service');
|
||||
let { data } = options;
|
||||
const { currentUser, transaction, runtimeContext } = options;
|
||||
|
||||
// Validate asset_type and mime_type match
|
||||
const assetType = data.asset_type;
|
||||
const mimeType = data.mime_type;
|
||||
@ -216,7 +224,12 @@ class AssetsService extends BaseService {
|
||||
data = await AssetsService.enrichStoredMediaMetadata(data);
|
||||
|
||||
// Call parent create
|
||||
const asset = await super.create(data, currentUser);
|
||||
const asset = await super.create({
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
// Note: Reversed video generation is handled by tour_pages.js when the video
|
||||
// is assigned to a navigation element. This avoids unnecessary processing
|
||||
@ -228,8 +241,15 @@ class AssetsService extends BaseService {
|
||||
/**
|
||||
* Update asset with MIME type validation and embed URL validation
|
||||
*/
|
||||
static async update(data, id, currentUser) {
|
||||
const existingAsset = await AssetsDBApi.findBy({ id });
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'Service');
|
||||
let { data } = options;
|
||||
const { id, currentUser, transaction, runtimeContext } = options;
|
||||
|
||||
const existingAsset = await AssetsDBApi.findBy(
|
||||
{ id },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
|
||||
// If updating asset_type or mime_type, validate they match
|
||||
if (data.asset_type || data.mime_type) {
|
||||
@ -258,7 +278,13 @@ class AssetsService extends BaseService {
|
||||
});
|
||||
|
||||
// Call parent update
|
||||
return super.update(data, id, currentUser);
|
||||
return super.update({
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -155,7 +155,9 @@ class Auth {
|
||||
try {
|
||||
await UsersDBApi.findBy({ id: currentUser.id }, { transaction });
|
||||
|
||||
await UsersDBApi.update(currentUser.id, data, {
|
||||
await UsersDBApi.update({
|
||||
id: currentUser.id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
@ -2,22 +2,40 @@ const db = require('../db/models');
|
||||
const Project_audio_tracksDBApi = require('../db/api/project_audio_tracks');
|
||||
const processFile = require('../middlewares/upload');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const {
|
||||
assertCreateOptions,
|
||||
assertDeleteByIdsOptions,
|
||||
assertIdOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../contracts/entity-options');
|
||||
const csv = require('csv-parser');
|
||||
const stream = require('stream');
|
||||
|
||||
module.exports = class Project_audio_tracksService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'Service');
|
||||
const {
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
const createdTrack = await Project_audio_tracksDBApi.create(data, {
|
||||
const createdTrack = await Project_audio_tracksDBApi.create({
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return createdTrack;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -57,12 +75,23 @@ module.exports = class Project_audio_tracksService {
|
||||
}
|
||||
}
|
||||
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'Service');
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
let project_audio_tracks = await Project_audio_tracksDBApi.findBy(
|
||||
{ id },
|
||||
{ transaction },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
|
||||
if (!project_audio_tracks) {
|
||||
@ -70,47 +99,72 @@ module.exports = class Project_audio_tracksService {
|
||||
}
|
||||
|
||||
const updatedProject_audio_tracks =
|
||||
await Project_audio_tracksDBApi.update(id, data, {
|
||||
await Project_audio_tracksDBApi.update({
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return updatedProject_audio_tracks;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async deleteByIds(options) {
|
||||
assertDeleteByIdsOptions(options, 'Service');
|
||||
const {
|
||||
ids,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
await Project_audio_tracksDBApi.deleteByIds(ids, {
|
||||
await Project_audio_tracksDBApi.deleteByIds({
|
||||
ids,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async remove(options) {
|
||||
assertIdOptions(options, 'Service', 'remove');
|
||||
const {
|
||||
id,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
await Project_audio_tracksDBApi.remove(id, {
|
||||
await Project_audio_tracksDBApi.remove({
|
||||
id,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,25 +2,40 @@ const db = require('../db/models');
|
||||
const Project_transition_settingsDBApi = require('../db/api/project_transition_settings');
|
||||
const processFile = require('../middlewares/upload');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const {
|
||||
assertCreateOptions,
|
||||
assertDeleteByIdsOptions,
|
||||
assertIdOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../contracts/entity-options');
|
||||
const csv = require('csv-parser');
|
||||
const stream = require('stream');
|
||||
|
||||
module.exports = class Project_transition_settingsService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const createdRecord = await Project_transition_settingsDBApi.create(
|
||||
data,
|
||||
{
|
||||
currentUser,
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'Service');
|
||||
const {
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
await transaction.commit();
|
||||
try {
|
||||
const createdRecord = await Project_transition_settingsDBApi.create({
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return createdRecord;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -60,63 +75,95 @@ module.exports = class Project_transition_settingsService {
|
||||
}
|
||||
}
|
||||
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'Service');
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
let record = await Project_transition_settingsDBApi.findBy(
|
||||
{ id },
|
||||
{ transaction },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw new ValidationError('project_transition_settingsNotFound');
|
||||
}
|
||||
|
||||
const updatedRecord = await Project_transition_settingsDBApi.update(
|
||||
const updatedRecord = await Project_transition_settingsDBApi.update({
|
||||
id,
|
||||
data,
|
||||
{
|
||||
currentUser,
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return updatedRecord;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async deleteByIds(options) {
|
||||
assertDeleteByIdsOptions(options, 'Service');
|
||||
const {
|
||||
ids,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
await Project_transition_settingsDBApi.deleteByIds(ids, {
|
||||
await Project_transition_settingsDBApi.deleteByIds({
|
||||
ids,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async remove(options) {
|
||||
assertIdOptions(options, 'Service', 'remove');
|
||||
const {
|
||||
id,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
await Project_transition_settingsDBApi.remove(id, {
|
||||
await Project_transition_settingsDBApi.remove({
|
||||
id,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const db = require('../db/models');
|
||||
const Project_ui_control_settingsDBApi = require('../db/api/project_ui_control_settings');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const { assertIdOptions } = require('../contracts/entity-options');
|
||||
|
||||
module.exports = class Project_ui_control_settingsService {
|
||||
static async findByProjectAndEnvironment(
|
||||
@ -34,27 +35,38 @@ module.exports = class Project_ui_control_settingsService {
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async remove(options) {
|
||||
assertIdOptions(options, 'Service', 'remove');
|
||||
const {
|
||||
id,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
const record = await Project_ui_control_settingsDBApi.findBy(
|
||||
{ id },
|
||||
{ transaction },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
throw new ValidationError('project_ui_control_settingsNotFound');
|
||||
}
|
||||
|
||||
await Project_ui_control_settingsDBApi.remove(id, {
|
||||
await Project_ui_control_settingsDBApi.remove({
|
||||
id,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,10 @@ const { v4: uuidv4 } = require('uuid');
|
||||
const db = require('../db/models');
|
||||
const ProjectsDBApi = require('../db/api/projects');
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
const {
|
||||
assertCreateOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../contracts/entity-options');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const FileService = require('./file');
|
||||
const { logger } = require('../utils/logger');
|
||||
@ -147,8 +151,18 @@ class ProjectsService extends BaseProjectsService {
|
||||
/**
|
||||
* Create project with slug validation
|
||||
*/
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'Service');
|
||||
const {
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
if (data.slug) {
|
||||
data.slug = await ProjectsService.validateSlugUniqueness(
|
||||
@ -158,15 +172,17 @@ class ProjectsService extends BaseProjectsService {
|
||||
);
|
||||
}
|
||||
|
||||
const project = await ProjectsDBApi.create(data, {
|
||||
const project = await ProjectsDBApi.create({
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return project;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -174,10 +190,24 @@ class ProjectsService extends BaseProjectsService {
|
||||
/**
|
||||
* Update project with slug validation
|
||||
*/
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'Service');
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
const project = await ProjectsDBApi.findBy({ id }, { transaction });
|
||||
const project = await ProjectsDBApi.findBy(
|
||||
{ id },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
throw new ValidationError('projectsNotFound');
|
||||
@ -191,15 +221,18 @@ class ProjectsService extends BaseProjectsService {
|
||||
);
|
||||
}
|
||||
|
||||
const updated = await ProjectsDBApi.update(id, data, {
|
||||
const updated = await ProjectsDBApi.update({
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return updated;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -238,8 +271,8 @@ class ProjectsService extends BaseProjectsService {
|
||||
transaction,
|
||||
);
|
||||
|
||||
const clonedProject = await ProjectsDBApi.create(
|
||||
{
|
||||
const clonedProject = await ProjectsDBApi.create({
|
||||
data: {
|
||||
name: `${sourceProject.name} (Copy)`,
|
||||
slug: uniqueSlug,
|
||||
description: sourceProject.description,
|
||||
@ -249,8 +282,9 @@ class ProjectsService extends BaseProjectsService {
|
||||
design_width: sourceProject.design_width,
|
||||
design_height: sourceProject.design_height,
|
||||
},
|
||||
{ currentUser, transaction },
|
||||
);
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Phase B: Collect all copy operations
|
||||
|
||||
@ -8,6 +8,12 @@ const config = require('../config');
|
||||
const stream = require('stream');
|
||||
const { validateReadOnlySql } = require('../utils/sqlValidator');
|
||||
const { logger } = require('../utils/logger');
|
||||
const {
|
||||
assertCreateOptions,
|
||||
assertDeleteByIdsOptions,
|
||||
assertIdOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../contracts/entity-options');
|
||||
|
||||
const WIDGET_SQL_MAX_LENGTH = 5000;
|
||||
const WIDGET_SQL_MAX_ROWS = 1000;
|
||||
@ -48,20 +54,32 @@ module.exports = class RolesService {
|
||||
}
|
||||
}
|
||||
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'Service');
|
||||
const {
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
this.assertPublicRoleHasNoPermissions(data);
|
||||
|
||||
const createdRole = await RolesDBApi.create(data, {
|
||||
const createdRole = await RolesDBApi.create({
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return createdRole;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -104,10 +122,24 @@ module.exports = class RolesService {
|
||||
}
|
||||
}
|
||||
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'Service');
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
let roles = await RolesDBApi.findBy({ id }, { transaction });
|
||||
let roles = await RolesDBApi.findBy(
|
||||
{ id },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
|
||||
if (!roles) {
|
||||
throw new ValidationError('rolesNotFound');
|
||||
@ -115,47 +147,72 @@ module.exports = class RolesService {
|
||||
|
||||
this.assertPublicRoleHasNoPermissions(data, roles);
|
||||
|
||||
const updatedRoles = await RolesDBApi.update(id, data, {
|
||||
const updatedRoles = await RolesDBApi.update({
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return updatedRoles;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async deleteByIds(options) {
|
||||
assertDeleteByIdsOptions(options, 'Service');
|
||||
const {
|
||||
ids,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
await RolesDBApi.deleteByIds(ids, {
|
||||
await RolesDBApi.deleteByIds({
|
||||
ids,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async remove(options) {
|
||||
assertIdOptions(options, 'Service', 'remove');
|
||||
const {
|
||||
id,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
await RolesDBApi.remove(id, {
|
||||
await RolesDBApi.remove({
|
||||
id,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -197,18 +254,16 @@ module.exports = class RolesService {
|
||||
customization[key] = [widgetId];
|
||||
}
|
||||
|
||||
const newRole = await RolesDBApi.update(
|
||||
role.id,
|
||||
{
|
||||
const newRole = await RolesDBApi.update({
|
||||
id: role.id,
|
||||
data: {
|
||||
role_customization: JSON.stringify(customization),
|
||||
name: role.name,
|
||||
permissions: role.permissions,
|
||||
},
|
||||
{
|
||||
currentUser,
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
@ -249,18 +304,16 @@ module.exports = class RolesService {
|
||||
`${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`,
|
||||
);
|
||||
try {
|
||||
const result = await RolesDBApi.update(
|
||||
role.id,
|
||||
{
|
||||
const result = await RolesDBApi.update({
|
||||
id: role.id,
|
||||
data: {
|
||||
role_customization: JSON.stringify(customization),
|
||||
name: role.name,
|
||||
permissions: role.permissions,
|
||||
},
|
||||
{
|
||||
currentUser,
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
return result;
|
||||
|
||||
@ -9,6 +9,10 @@ const Tour_pagesDBApi = require('../db/api/tour_pages');
|
||||
const AssetsDBApi = require('../db/api/assets');
|
||||
const Asset_variantsDBApi = require('../db/api/asset_variants');
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
const {
|
||||
assertCreateOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../contracts/entity-options');
|
||||
const {
|
||||
downloadToBuffer,
|
||||
downloadToTempFile,
|
||||
@ -302,11 +306,12 @@ class TourPagesService extends BaseService {
|
||||
|
||||
const updatedPages = [];
|
||||
for (const [index, pageId] of orderedPageIds.entries()) {
|
||||
const page = await Tour_pagesDBApi.partialUpdate(
|
||||
pageId,
|
||||
{ sort_order: index + 1 },
|
||||
{ currentUser, transaction },
|
||||
);
|
||||
const page = await Tour_pagesDBApi.partialUpdate({
|
||||
id: pageId,
|
||||
data: { sort_order: index + 1 },
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
updatedPages.push(page);
|
||||
}
|
||||
|
||||
@ -429,7 +434,8 @@ class TourPagesService extends BaseService {
|
||||
currentUser,
|
||||
);
|
||||
|
||||
return Tour_pagesDBApi.create(processedPayload, {
|
||||
return Tour_pagesDBApi.create({
|
||||
data: processedPayload,
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
@ -439,7 +445,10 @@ class TourPagesService extends BaseService {
|
||||
/**
|
||||
* Create tour page - generate reversed videos if needed
|
||||
*/
|
||||
static async create(data, currentUser) {
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'Service');
|
||||
const { data, currentUser, transaction, runtimeContext } = options;
|
||||
|
||||
// Process reversed videos and get updated ui_schema_json
|
||||
const updatedData =
|
||||
await TourPagesService.processReversedVideosAndUpdateSchema(
|
||||
@ -447,15 +456,26 @@ class TourPagesService extends BaseService {
|
||||
currentUser,
|
||||
);
|
||||
|
||||
return super.create(updatedData, currentUser);
|
||||
return super.create({
|
||||
data: updatedData,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tour page - generate reversed videos if needed
|
||||
*/
|
||||
static async update(data, id, currentUser) {
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'Service');
|
||||
const { id, data, currentUser, transaction, runtimeContext } = options;
|
||||
|
||||
// Fetch existing page to get projectId (not included in update request body)
|
||||
const existingPage = await Tour_pagesDBApi.findBy({ id });
|
||||
const existingPage = await Tour_pagesDBApi.findBy(
|
||||
{ id },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
const projectId =
|
||||
existingPage?.projectId || data.projectId || data.project_id;
|
||||
|
||||
@ -466,7 +486,13 @@ class TourPagesService extends BaseService {
|
||||
currentUser,
|
||||
);
|
||||
|
||||
return super.update(updatedData, id, currentUser);
|
||||
return super.update({
|
||||
id,
|
||||
data: updatedData,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -828,11 +854,11 @@ class TourPagesService extends BaseService {
|
||||
|
||||
if (!pageModified) continue;
|
||||
|
||||
await Tour_pagesDBApi.partialUpdate(
|
||||
page.id,
|
||||
{ ui_schema_json: JSON.stringify(uiSchema) },
|
||||
{ currentUser },
|
||||
);
|
||||
await Tour_pagesDBApi.partialUpdate({
|
||||
id: page.id,
|
||||
data: { ui_schema_json: JSON.stringify(uiSchema) },
|
||||
currentUser,
|
||||
});
|
||||
pagesUpdated++;
|
||||
}
|
||||
|
||||
@ -914,16 +940,16 @@ class TourPagesService extends BaseService {
|
||||
});
|
||||
|
||||
// Create variant record
|
||||
await Asset_variantsDBApi.create(
|
||||
{
|
||||
await Asset_variantsDBApi.create({
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
variant_type: 'reversed',
|
||||
cdn_url: result.url,
|
||||
storage_key: reversedKey,
|
||||
size_mb: reversedBuffer.length / (1024 * 1024),
|
||||
},
|
||||
{ currentUser },
|
||||
);
|
||||
currentUser,
|
||||
});
|
||||
|
||||
log.info(
|
||||
{ reversedKey, size: reversedBuffer.length },
|
||||
@ -1034,11 +1060,11 @@ class TourPagesService extends BaseService {
|
||||
if (pageModified) {
|
||||
// Use partialUpdate to only update ui_schema_json field
|
||||
// This avoids the regular update's getFieldMapping which sets other fields to null
|
||||
await Tour_pagesDBApi.partialUpdate(
|
||||
page.id,
|
||||
{ ui_schema_json: JSON.stringify(uiSchema) },
|
||||
{ currentUser },
|
||||
);
|
||||
await Tour_pagesDBApi.partialUpdate({
|
||||
id: page.id,
|
||||
data: { ui_schema_json: JSON.stringify(uiSchema) },
|
||||
currentUser,
|
||||
});
|
||||
pagesUpdated++;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
const db = require('../db/models');
|
||||
const UsersDBApi = require('../db/api/users');
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
const {
|
||||
assertCreateOptions,
|
||||
assertIdOptions,
|
||||
assertUpdateOptions,
|
||||
} = require('../contracts/entity-options');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const config = require('../config');
|
||||
const AuthService = require('./auth');
|
||||
@ -126,8 +131,19 @@ class UsersService extends BaseUsersService {
|
||||
/**
|
||||
* Create user with email validation and optional invitation
|
||||
*/
|
||||
static async create(data, currentUser, sendInvitationEmails = true, host) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async create(options) {
|
||||
assertCreateOptions(options, 'Service');
|
||||
const {
|
||||
data,
|
||||
currentUser,
|
||||
sendInvitationEmails = true,
|
||||
host,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
const email = data.email;
|
||||
|
||||
try {
|
||||
@ -157,15 +173,20 @@ class UsersService extends BaseUsersService {
|
||||
|
||||
if (existingUser?.deletedAt) {
|
||||
await existingUser.restore({ transaction });
|
||||
user = await UsersDBApi.update(existingUser.id, sanitizedData, {
|
||||
user = await UsersDBApi.update({
|
||||
id: existingUser.id,
|
||||
data: sanitizedData,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
} else {
|
||||
user = await UsersDBApi.create(
|
||||
{ data: sanitizedData },
|
||||
{ currentUser, transaction },
|
||||
);
|
||||
user = await UsersDBApi.create({
|
||||
data: sanitizedData,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
}
|
||||
|
||||
await this.createProductionPresentationAccessForPublicUser({
|
||||
@ -174,7 +195,7 @@ class UsersService extends BaseUsersService {
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
|
||||
// Send invitation email after successful commit
|
||||
if (sendInvitationEmails) {
|
||||
@ -188,16 +209,29 @@ class UsersService extends BaseUsersService {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
static async update(options) {
|
||||
assertUpdateOptions(options, 'Service');
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
currentUser,
|
||||
transaction: externalTransaction,
|
||||
runtimeContext,
|
||||
} = options;
|
||||
const transaction =
|
||||
externalTransaction || (await db.sequelize.transaction());
|
||||
const ownsTransaction = !externalTransaction;
|
||||
|
||||
try {
|
||||
const existingUser = await UsersDBApi.findBy({ id }, { transaction });
|
||||
const existingUser = await UsersDBApi.findBy(
|
||||
{ id },
|
||||
{ transaction, runtimeContext },
|
||||
);
|
||||
|
||||
if (!existingUser) {
|
||||
throw new ValidationError('UsersNotFound');
|
||||
@ -229,9 +263,12 @@ class UsersService extends BaseUsersService {
|
||||
? { ...data, custom_permissions: [] }
|
||||
: data;
|
||||
|
||||
const user = await UsersDBApi.update(id, sanitizedData, {
|
||||
const user = await UsersDBApi.update({
|
||||
id,
|
||||
data: sanitizedData,
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
if (
|
||||
@ -248,10 +285,10 @@ class UsersService extends BaseUsersService {
|
||||
});
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
if (ownsTransaction) await transaction.commit();
|
||||
return user;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
if (ownsTransaction) await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -259,7 +296,10 @@ class UsersService extends BaseUsersService {
|
||||
/**
|
||||
* Remove user with self-deletion and permission checks
|
||||
*/
|
||||
static async remove(id, currentUser) {
|
||||
static async remove(options) {
|
||||
assertIdOptions(options, 'Service', 'remove');
|
||||
const { id, currentUser, transaction, runtimeContext } = options;
|
||||
|
||||
if (currentUser.id === id) {
|
||||
throw new ValidationError('iam.errors.deletingHimself');
|
||||
}
|
||||
@ -269,7 +309,7 @@ class UsersService extends BaseUsersService {
|
||||
}
|
||||
|
||||
// Delegate to parent (factory) implementation
|
||||
return super.remove(id, currentUser);
|
||||
return super.remove({ id, currentUser, transaction, runtimeContext });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
486
backend/tests/update-contracts.test.js
Normal file
486
backend/tests/update-contracts.test.js
Normal file
@ -0,0 +1,486 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const db = require('../src/db/models');
|
||||
const GenericDBApi = require('../src/db/api/base.api');
|
||||
const { createEntityService } = require('../src/factories/service.factory');
|
||||
|
||||
test('GenericDBApi.update uses object signature and forwards update context', async () => {
|
||||
const calls = {};
|
||||
const transaction = { id: 'tx-db-api' };
|
||||
const currentUser = { id: 'user-1' };
|
||||
const record = {
|
||||
async update(payload, options) {
|
||||
calls.update = { payload, options };
|
||||
},
|
||||
async setTags(value, options) {
|
||||
calls.setTags = { value, options };
|
||||
},
|
||||
};
|
||||
|
||||
class TestDBApi extends GenericDBApi {
|
||||
static get MODEL() {
|
||||
return {
|
||||
rawAttributes: {},
|
||||
getTableName: () => 'test_records',
|
||||
async findByPk(id, options) {
|
||||
calls.findByPk = { id, options };
|
||||
return record;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get ASSOCIATIONS() {
|
||||
return [{ field: 'tags', setter: 'setTags', isArray: true }];
|
||||
}
|
||||
}
|
||||
|
||||
const result = await TestDBApi.update({
|
||||
id: 'record-1',
|
||||
data: { name: 'Updated', tags: ['a', 'b'], skipped: undefined },
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
assert.equal(result, record);
|
||||
assert.deepEqual(calls.findByPk, {
|
||||
id: 'record-1',
|
||||
options: { transaction },
|
||||
});
|
||||
assert.deepEqual(calls.update, {
|
||||
payload: {
|
||||
updatedById: 'user-1',
|
||||
name: 'Updated',
|
||||
tags: ['a', 'b'],
|
||||
},
|
||||
options: { transaction },
|
||||
});
|
||||
assert.deepEqual(calls.setTags, {
|
||||
value: ['a', 'b'],
|
||||
options: { transaction },
|
||||
});
|
||||
});
|
||||
|
||||
test('GenericDBApi.update rejects positional signature', async () => {
|
||||
class TestDBApi extends GenericDBApi {
|
||||
static get MODEL() {
|
||||
return {
|
||||
rawAttributes: {},
|
||||
getTableName: () => 'test_records',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
() => TestDBApi.update('record-1', { name: 'Updated' }, {}),
|
||||
/DBApi\.update expects an options object/,
|
||||
);
|
||||
});
|
||||
|
||||
test('GenericDBApi.create uses object signature and forwards create context', async () => {
|
||||
const calls = {};
|
||||
const transaction = { id: 'tx-create' };
|
||||
const currentUser = { id: 'user-1' };
|
||||
const record = {
|
||||
id: 'record-1',
|
||||
async setTags(value, options) {
|
||||
calls.setTags = { value, options };
|
||||
},
|
||||
};
|
||||
|
||||
class TestDBApi extends GenericDBApi {
|
||||
static get MODEL() {
|
||||
return {
|
||||
rawAttributes: {},
|
||||
getTableName: () => 'test_records',
|
||||
async create(payload, options) {
|
||||
calls.create = { payload, options };
|
||||
return record;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static get ASSOCIATIONS() {
|
||||
return [{ field: 'tags', setter: 'setTags', isArray: true }];
|
||||
}
|
||||
}
|
||||
|
||||
const result = await TestDBApi.create({
|
||||
data: { name: 'Created', tags: ['a', 'b'] },
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
assert.equal(result, record);
|
||||
assert.deepEqual(calls.create, {
|
||||
payload: {
|
||||
name: 'Created',
|
||||
tags: ['a', 'b'],
|
||||
importHash: null,
|
||||
createdById: 'user-1',
|
||||
updatedById: 'user-1',
|
||||
},
|
||||
options: { transaction },
|
||||
});
|
||||
assert.deepEqual(calls.setTags, {
|
||||
value: ['a', 'b'],
|
||||
options: { transaction },
|
||||
});
|
||||
});
|
||||
|
||||
test('GenericDBApi.create rejects positional signature', async () => {
|
||||
class TestDBApi extends GenericDBApi {
|
||||
static get MODEL() {
|
||||
return {
|
||||
rawAttributes: {},
|
||||
getTableName: () => 'test_records',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
() => TestDBApi.create({ name: 'Created' }, {}),
|
||||
/DBApi\.create requires \{ data \}/,
|
||||
);
|
||||
});
|
||||
|
||||
test('GenericDBApi.partialUpdate uses object signature and only updates defined fields', async () => {
|
||||
const calls = {};
|
||||
const transaction = { id: 'tx-partial' };
|
||||
const currentUser = { id: 'user-1' };
|
||||
const record = {
|
||||
async update(payload, options) {
|
||||
calls.update = { payload, options };
|
||||
},
|
||||
};
|
||||
|
||||
class TestDBApi extends GenericDBApi {
|
||||
static get MODEL() {
|
||||
return {
|
||||
rawAttributes: {},
|
||||
getTableName: () => 'test_records',
|
||||
async findByPk(id, options) {
|
||||
calls.findByPk = { id, options };
|
||||
return record;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = await TestDBApi.partialUpdate({
|
||||
id: 'record-1',
|
||||
data: { name: 'Updated', skipped: undefined },
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
assert.equal(result, record);
|
||||
assert.deepEqual(calls.findByPk, {
|
||||
id: 'record-1',
|
||||
options: { transaction },
|
||||
});
|
||||
assert.deepEqual(calls.update, {
|
||||
payload: {
|
||||
updatedById: 'user-1',
|
||||
name: 'Updated',
|
||||
},
|
||||
options: { transaction },
|
||||
});
|
||||
});
|
||||
|
||||
test('GenericDBApi.partialUpdate rejects positional signature', async () => {
|
||||
class TestDBApi extends GenericDBApi {
|
||||
static get MODEL() {
|
||||
return {
|
||||
rawAttributes: {},
|
||||
getTableName: () => 'test_records',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
() => TestDBApi.partialUpdate('record-1', { name: 'Updated' }, {}),
|
||||
/DBApi\.update expects an options object/,
|
||||
);
|
||||
});
|
||||
|
||||
test('createEntityService update uses object signature and manages own transaction', async () => {
|
||||
const originalTransaction = db.sequelize.transaction;
|
||||
const calls = {};
|
||||
const transaction = {
|
||||
async commit() {
|
||||
calls.committed = true;
|
||||
},
|
||||
async rollback() {
|
||||
calls.rolledBack = true;
|
||||
},
|
||||
};
|
||||
|
||||
db.sequelize.transaction = async () => transaction;
|
||||
|
||||
const DBApi = {
|
||||
async findBy(where, options) {
|
||||
calls.findBy = { where, options };
|
||||
return { id: where.id };
|
||||
},
|
||||
async update(options) {
|
||||
calls.update = options;
|
||||
return { id: options.id, ...options.data };
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const Service = createEntityService(DBApi, { entityName: 'TestEntity' });
|
||||
const currentUser = { id: 'user-1' };
|
||||
const runtimeContext = { environment: 'dev' };
|
||||
|
||||
const result = await Service.update({
|
||||
id: 'record-1',
|
||||
data: { name: 'Updated' },
|
||||
currentUser,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { id: 'record-1', name: 'Updated' });
|
||||
assert.deepEqual(calls.findBy, {
|
||||
where: { id: 'record-1' },
|
||||
options: { transaction, runtimeContext },
|
||||
});
|
||||
assert.deepEqual(calls.update, {
|
||||
id: 'record-1',
|
||||
data: { name: 'Updated' },
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
assert.equal(calls.committed, true);
|
||||
assert.equal(calls.rolledBack, undefined);
|
||||
} finally {
|
||||
db.sequelize.transaction = originalTransaction;
|
||||
}
|
||||
});
|
||||
|
||||
test('createEntityService update rejects positional signature', async () => {
|
||||
const Service = createEntityService(
|
||||
{
|
||||
async findBy() {
|
||||
throw new Error('should not be called');
|
||||
},
|
||||
async update() {
|
||||
throw new Error('should not be called');
|
||||
},
|
||||
},
|
||||
{ entityName: 'TestEntity' },
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => Service.update({ name: 'Updated' }, 'record-1', { id: 'user-1' }),
|
||||
/Service\.update requires \{ id, data \}/,
|
||||
);
|
||||
});
|
||||
|
||||
test('createEntityService create, deleteByIds, and remove use object signatures', async () => {
|
||||
const originalTransaction = db.sequelize.transaction;
|
||||
const calls = {};
|
||||
const transaction = {
|
||||
async commit() {
|
||||
calls.commits = (calls.commits || 0) + 1;
|
||||
},
|
||||
async rollback() {
|
||||
calls.rollbacks = (calls.rollbacks || 0) + 1;
|
||||
},
|
||||
};
|
||||
db.sequelize.transaction = async () => transaction;
|
||||
|
||||
const DBApi = {
|
||||
async create(options) {
|
||||
calls.create = options;
|
||||
return { id: 'created-1', ...options.data };
|
||||
},
|
||||
async deleteByIds(options) {
|
||||
calls.deleteByIds = options;
|
||||
},
|
||||
async remove(options) {
|
||||
calls.remove = options;
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const Service = createEntityService(DBApi, { entityName: 'TestEntity' });
|
||||
const currentUser = { id: 'user-1' };
|
||||
const runtimeContext = { environment: 'stage' };
|
||||
|
||||
const created = await Service.create({
|
||||
data: { name: 'Created' },
|
||||
currentUser,
|
||||
runtimeContext,
|
||||
});
|
||||
await Service.deleteByIds({
|
||||
ids: ['record-1', 'record-2'],
|
||||
currentUser,
|
||||
runtimeContext,
|
||||
});
|
||||
await Service.remove({
|
||||
id: 'record-1',
|
||||
currentUser,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
assert.deepEqual(created, { id: 'created-1', name: 'Created' });
|
||||
assert.deepEqual(calls.create, {
|
||||
data: { name: 'Created' },
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
assert.deepEqual(calls.deleteByIds, {
|
||||
ids: ['record-1', 'record-2'],
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
assert.deepEqual(calls.remove, {
|
||||
id: 'record-1',
|
||||
currentUser,
|
||||
transaction,
|
||||
runtimeContext,
|
||||
});
|
||||
assert.equal(calls.commits, 3);
|
||||
assert.equal(calls.rollbacks, undefined);
|
||||
} finally {
|
||||
db.sequelize.transaction = originalTransaction;
|
||||
}
|
||||
});
|
||||
|
||||
test('createEntityService create, deleteByIds, and remove reject positional signatures', async () => {
|
||||
const Service = createEntityService(
|
||||
{
|
||||
async create() {
|
||||
throw new Error('should not be called');
|
||||
},
|
||||
async deleteByIds() {
|
||||
throw new Error('should not be called');
|
||||
},
|
||||
async remove() {
|
||||
throw new Error('should not be called');
|
||||
},
|
||||
},
|
||||
{ entityName: 'TestEntity' },
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => Service.create({ name: 'Created' }, { id: 'user-1' }),
|
||||
/Service\.create requires \{ data \}/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() => Service.deleteByIds(['record-1'], { id: 'user-1' }),
|
||||
/Service\.deleteByIds expects an options object/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() => Service.remove('record-1', { id: 'user-1' }),
|
||||
/Service\.remove expects an options object/,
|
||||
);
|
||||
});
|
||||
|
||||
test('GenericDBApi deleteByIds, remove, and findAllAutocomplete use object signatures', async () => {
|
||||
const calls = {};
|
||||
const transaction = { id: 'tx-db-api' };
|
||||
const currentUser = { id: 'user-1' };
|
||||
const deletedRecords = [
|
||||
{
|
||||
id: 'record-1',
|
||||
async update(payload, options) {
|
||||
calls.deletedUpdate = { payload, options };
|
||||
},
|
||||
async destroy(options) {
|
||||
calls.deletedDestroy = options;
|
||||
},
|
||||
},
|
||||
];
|
||||
const removedRecord = {
|
||||
id: 'record-2',
|
||||
async update(payload, options) {
|
||||
calls.removedUpdate = { payload, options };
|
||||
},
|
||||
async destroy(options) {
|
||||
calls.removedDestroy = options;
|
||||
},
|
||||
};
|
||||
const autocompleteRecords = [{ id: 'record-3', name: 'Alpha' }];
|
||||
|
||||
class TestDBApi extends GenericDBApi {
|
||||
static get MODEL() {
|
||||
return {
|
||||
rawAttributes: {},
|
||||
getTableName: () => 'test_records',
|
||||
async findAll(options) {
|
||||
calls.findAll = options;
|
||||
return options.attributes ? autocompleteRecords : deletedRecords;
|
||||
},
|
||||
async findByPk(id, options) {
|
||||
calls.findByPk = { id, options };
|
||||
return removedRecord;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await TestDBApi.deleteByIds({
|
||||
ids: ['record-1'],
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
const removed = await TestDBApi.remove({
|
||||
id: 'record-2',
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
const autocomplete = await TestDBApi.findAllAutocomplete(
|
||||
{ query: 'Alpha', limit: 5, offset: 0 },
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
assert.equal(deleted, deletedRecords);
|
||||
assert.equal(removed, removedRecord);
|
||||
assert.deepEqual(calls.deletedUpdate, {
|
||||
payload: { deletedBy: 'user-1' },
|
||||
options: { transaction },
|
||||
});
|
||||
assert.deepEqual(calls.deletedDestroy, { transaction });
|
||||
assert.deepEqual(calls.findByPk, {
|
||||
id: 'record-2',
|
||||
options: { transaction },
|
||||
});
|
||||
assert.deepEqual(calls.removedUpdate, {
|
||||
payload: { deletedBy: 'user-1' },
|
||||
options: { transaction },
|
||||
});
|
||||
assert.deepEqual(calls.removedDestroy, { transaction });
|
||||
assert.deepEqual(autocomplete, [{ id: 'record-3', label: 'Alpha' }]);
|
||||
assert.equal(calls.findAll.limit, 5);
|
||||
assert.equal(calls.findAll.transaction, transaction);
|
||||
});
|
||||
|
||||
test('GenericDBApi deleteByIds, remove, and findAllAutocomplete reject positional signatures', async () => {
|
||||
class TestDBApi extends GenericDBApi {
|
||||
static get MODEL() {
|
||||
return {
|
||||
rawAttributes: {},
|
||||
getTableName: () => 'test_records',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
() => TestDBApi.deleteByIds(['record-1'], {}),
|
||||
/DBApi\.deleteByIds expects an options object/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() => TestDBApi.remove('record-1', {}),
|
||||
/DBApi\.remove expects an options object/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() => TestDBApi.findAllAutocomplete('Alpha', 5, 0),
|
||||
/DBApi\.findAllAutocomplete expects an options object/,
|
||||
);
|
||||
});
|
||||
@ -46,7 +46,7 @@ export function useProjectSelector({
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
'/projects?limit=100&page=0&sort=desc&field=updatedAt',
|
||||
'/projects?limit=100&page=1&sort=desc&field=updatedAt',
|
||||
);
|
||||
const rows = Array.isArray(response?.data?.rows)
|
||||
? response.data.rows
|
||||
|
||||
@ -61,7 +61,7 @@ async function fetchRelationOptions(
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios(`/${entityRef}/autocomplete?limit=100`);
|
||||
const response = await axios(`/${entityRef}/autocomplete?limit=50`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@ -159,15 +159,34 @@ function buildColumn(
|
||||
|
||||
if (col.type === 'singleSelectRelation' && col.entityRef) {
|
||||
const singleSelectColumn = baseColumn as GridSingleSelectColDef;
|
||||
const valueOptions = valueOptionsMap.get(col.entityRef) || [];
|
||||
|
||||
singleSelectColumn.type = 'singleSelect';
|
||||
singleSelectColumn.sortable = false;
|
||||
singleSelectColumn.getOptionValue = (value: { id?: string }) => value?.id;
|
||||
singleSelectColumn.getOptionLabel = (value: { label?: string }) =>
|
||||
value?.label;
|
||||
singleSelectColumn.valueOptions = valueOptionsMap.get(col.entityRef) || [];
|
||||
singleSelectColumn.valueGetter = (value: { id?: string } | string | null) =>
|
||||
singleSelectColumn.valueOptions = valueOptions;
|
||||
singleSelectColumn.valueGetter = (
|
||||
value: { id?: string; label?: string; name?: string } | string | null,
|
||||
) =>
|
||||
(typeof value === 'object' && value !== null ? value?.id : value) ??
|
||||
value;
|
||||
singleSelectColumn.valueFormatter = (value: unknown) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const relationValue = value as {
|
||||
id?: string;
|
||||
label?: string;
|
||||
name?: string;
|
||||
};
|
||||
return (
|
||||
relationValue.label || relationValue.name || relationValue.id || ''
|
||||
);
|
||||
}
|
||||
|
||||
const option = valueOptions.find((item) => item.id === value);
|
||||
return option?.label || String(value || '');
|
||||
};
|
||||
return singleSelectColumn;
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ const DataGridMultiSelect = (props: GridRenderEditCellParams & Props) => {
|
||||
const [options, setOptions] = useState([]);
|
||||
|
||||
async function callApi(entityName: string) {
|
||||
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
||||
const data = await axios(`/${entityName}/autocomplete?limit=50`);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ export const SelectField = ({
|
||||
onOptionChange,
|
||||
}) => {
|
||||
const [value, setValue] = useState(null);
|
||||
const PAGE_SIZE = 100;
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
useEffect(() => {
|
||||
if (options?.id && field?.value?.id) {
|
||||
|
||||
@ -23,7 +23,7 @@ export const SelectFieldMany = ({
|
||||
}) => {
|
||||
const [value, setValue] = useState([]);
|
||||
const appliedOptionsSignatureRef = useRef<string | null>(null);
|
||||
const PAGE_SIZE = 100;
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
useEffect(() => {
|
||||
if (field.value?.[0] && typeof field.value[0] !== 'string') {
|
||||
|
||||
@ -176,7 +176,7 @@ const TourFlowManager = () => {
|
||||
const canDeleteTransition = hasPermission(currentUser, 'DELETE_TRANSITIONS');
|
||||
|
||||
const loadProjects = useCallback(async () => {
|
||||
const response = await axios.get('/projects/autocomplete?limit=100');
|
||||
const response = await axios.get('/projects/autocomplete?limit=50');
|
||||
const projectOptions = Array.isArray(response?.data) ? response.data : [];
|
||||
setProjects(projectOptions);
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ export const RoleSelect = ({
|
||||
currentUser,
|
||||
}) => {
|
||||
const [value, setValue] = useState(null);
|
||||
const PAGE_SIZE = 100;
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (currentUser.app_role.id) {
|
||||
|
||||
@ -32,7 +32,7 @@ export function useElementDefaultsQuery(projectId: string | undefined) {
|
||||
Partial<Record<CanvasElementType, Partial<CanvasElement>>>
|
||||
> => {
|
||||
const response = await axios.get<ElementDefaultsResponse>(
|
||||
`project-element-defaults?projectId=${projectId}&limit=200&page=0&sort=asc&field=sort_order`,
|
||||
`project-element-defaults?projectId=${projectId}&limit=200&page=1&sort=asc&field=sort_order`,
|
||||
);
|
||||
|
||||
// Process and normalize the defaults
|
||||
|
||||
@ -107,7 +107,7 @@ const AssetsTablesPage = () => {
|
||||
if (!projectId) return;
|
||||
dispatch(
|
||||
fetchAssets({
|
||||
query: `?page=0&sort=desc&field=createdAt&projectId=${projectId}`,
|
||||
query: `?page=1&sort=desc&field=createdAt&projectId=${projectId}`,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
@ -134,7 +134,7 @@ const ElementTypeDefaultsPage = () => {
|
||||
setErrorMessage('');
|
||||
|
||||
const response = await axios.get(
|
||||
'/element-type-defaults?limit=1000&page=0&sort=asc&field=sort_order',
|
||||
'/element-type-defaults?limit=1000&page=1&sort=asc&field=sort_order',
|
||||
);
|
||||
|
||||
const nextRows: ElementTypeDefault[] = Array.isArray(response?.data?.rows)
|
||||
|
||||
@ -64,7 +64,7 @@ const ProjectElementDefaultsPage = () => {
|
||||
setErrorMessage('');
|
||||
|
||||
const response = await axios.get(
|
||||
`/project-element-defaults?projectId=${projectId}&limit=1000&page=0&sort=asc&field=sort_order`,
|
||||
`/project-element-defaults?projectId=${projectId}&limit=1000&page=1&sort=asc&field=sort_order`,
|
||||
);
|
||||
|
||||
const nextRows: ProjectElementDefault[] = Array.isArray(
|
||||
|
||||
@ -171,7 +171,7 @@ const ProjectElementDefaultDetailsPage = () => {
|
||||
if (nextItem.projectId) {
|
||||
try {
|
||||
const assetsResponse = await axios.get(
|
||||
`/assets?limit=500&page=0&sort=desc&field=createdAt&projectId=${nextItem.projectId}`,
|
||||
`/assets?limit=500&page=1&sort=desc&field=createdAt&projectId=${nextItem.projectId}`,
|
||||
);
|
||||
const assetRows: ConstructorAsset[] = Array.isArray(
|
||||
assetsResponse?.data?.rows,
|
||||
|
||||
@ -75,7 +75,7 @@ const EditProjectsPage = () => {
|
||||
setIsLoadingLogoAssets(true);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/assets?limit=500&page=0&sort=desc&field=createdAt&projectId=${projectId}`,
|
||||
`/assets?limit=500&page=1&sort=desc&field=createdAt&projectId=${projectId}`,
|
||||
);
|
||||
const rows = Array.isArray(response?.data?.rows)
|
||||
? response.data.rows
|
||||
|
||||
@ -41,7 +41,7 @@ const ProjectsListPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
fetchProjects({ query: '?limit=100&page=0&sort=desc&field=updatedAt' }),
|
||||
fetchProjects({ query: '?limit=100&page=1&sort=desc&field=updatedAt' }),
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user