improved global-project-element scopes separation

This commit is contained in:
Dmitri 2026-03-27 15:58:15 +04:00
parent baef1fca2f
commit b8f2274572
131 changed files with 9141 additions and 9737 deletions

View File

@ -1,10 +1,10 @@
"use strict";
'use strict';
const fs = require("fs");
const path = require("path");
const http = require("http");
const https = require("https");
const { URL } = require("url");
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const { URL } = require('url');
let CONFIG_CACHE = null;
@ -40,7 +40,7 @@ async function createResponse(params, options = {}) {
if (!Array.isArray(payload.input) || payload.input.length === 0) {
return {
success: false,
error: "input_missing",
error: 'input_missing',
message: 'Parameter "input" is required and must be a non-empty array.',
};
}
@ -56,7 +56,7 @@ async function createResponse(params, options = {}) {
}
const data = initial.data;
if (data && typeof data === "object" && data.ai_request_id) {
if (data && typeof data === 'object' && data.ai_request_id) {
const pollTimeout = Number(options.poll_timeout ?? 300);
const pollInterval = Number(options.poll_interval ?? 5);
return await awaitResponse(data.ai_request_id, {
@ -78,16 +78,16 @@ async function request(pathValue, payload = {}, options = {}) {
if (!resolvedPath) {
return {
success: false,
error: "project_id_missing",
message: "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.",
error: 'project_id_missing',
message: 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.',
};
}
if (!cfg.projectUuid) {
return {
success: false,
error: "project_uuid_missing",
message: "PROJECT_UUID is not defined; aborting AI request.",
error: 'project_uuid_missing',
message: 'PROJECT_UUID is not defined; aborting AI request.',
};
}
@ -101,21 +101,21 @@ async function request(pathValue, payload = {}, options = {}) {
const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls);
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
Accept: 'application/json',
'Content-Type': 'application/json',
[cfg.projectHeader]: cfg.projectUuid,
};
if (Array.isArray(options.headers)) {
for (const header of options.headers) {
if (typeof header === "string" && header.includes(":")) {
const [name, value] = header.split(":", 2);
if (typeof header === 'string' && header.includes(':')) {
const [name, value] = header.split(':', 2);
headers[name.trim()] = value.trim();
}
}
}
const body = JSON.stringify(bodyPayload);
return sendRequest(url, "POST", body, headers, timeout, verifyTls);
return sendRequest(url, 'POST', body, headers, timeout, verifyTls);
}
async function fetchStatus(aiRequestId, options = {}) {
@ -123,8 +123,8 @@ async function fetchStatus(aiRequestId, options = {}) {
if (!cfg.projectUuid) {
return {
success: false,
error: "project_uuid_missing",
message: "PROJECT_UUID is not defined; aborting status check.",
error: 'project_uuid_missing',
message: 'PROJECT_UUID is not defined; aborting status check.',
};
}
@ -134,19 +134,19 @@ async function fetchStatus(aiRequestId, options = {}) {
const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls);
const headers = {
Accept: "application/json",
Accept: 'application/json',
[cfg.projectHeader]: cfg.projectUuid,
};
if (Array.isArray(options.headers)) {
for (const header of options.headers) {
if (typeof header === "string" && header.includes(":")) {
const [name, value] = header.split(":", 2);
if (typeof header === 'string' && header.includes(':')) {
const [name, value] = header.split(':', 2);
headers[name.trim()] = value.trim();
}
}
}
return sendRequest(url, "GET", null, headers, timeout, verifyTls);
return sendRequest(url, 'GET', null, headers, timeout, verifyTls);
}
async function awaitResponse(aiRequestId, options = {}) {
@ -165,8 +165,8 @@ async function awaitResponse(aiRequestId, options = {}) {
if (statusResp.success) {
const data = statusResp.data || {};
if (data && typeof data === "object") {
if (data.status === "success") {
if (data && typeof data === 'object') {
if (data.status === 'success') {
isPending = false;
return {
success: true,
@ -174,12 +174,12 @@ async function awaitResponse(aiRequestId, options = {}) {
data: data.response || data,
};
}
if (data.status === "failed") {
if (data.status === 'failed') {
isPending = false;
return {
success: false,
status: 500,
error: String(data.error || "AI request failed"),
error: String(data.error || 'AI request failed'),
data,
};
}
@ -191,8 +191,8 @@ async function awaitResponse(aiRequestId, options = {}) {
if (Date.now() >= deadline) {
return {
success: false,
error: "timeout",
message: "Timed out waiting for AI response.",
error: 'timeout',
message: 'Timed out waiting for AI response.',
};
}
@ -201,13 +201,14 @@ async function awaitResponse(aiRequestId, options = {}) {
}
function extractText(response) {
const payload = response && typeof response === "object" ? response.data || response : null;
if (!payload || typeof payload !== "object") {
return "";
const payload =
response && typeof response === 'object' ? response.data || response : null;
if (!payload || typeof payload !== 'object') {
return '';
}
if (Array.isArray(payload.output)) {
let combined = "";
let combined = '';
for (const item of payload.output) {
if (!item || !Array.isArray(item.content)) {
continue;
@ -215,9 +216,9 @@ function extractText(response) {
for (const block of item.content) {
if (
block &&
typeof block === "object" &&
block.type === "output_text" &&
typeof block.text === "string" &&
typeof block === 'object' &&
block.type === 'output_text' &&
typeof block.text === 'string' &&
block.text.length > 0
) {
combined += block.text;
@ -233,32 +234,38 @@ function extractText(response) {
payload.choices &&
payload.choices[0] &&
payload.choices[0].message &&
typeof payload.choices[0].message.content === "string"
typeof payload.choices[0].message.content === 'string'
) {
return payload.choices[0].message.content;
}
return "";
return '';
}
function decodeJsonFromResponse(response) {
const text = extractText(response);
if (!text) {
throw new Error("No text found in AI response.");
throw new Error('No text found in AI response.');
}
const parsed = parseJson(text);
if (parsed.ok && parsed.value && typeof parsed.value === "object") {
if (parsed.ok && parsed.value && typeof parsed.value === 'object') {
return parsed.value;
}
const stripped = stripJsonFence(text);
if (stripped !== text) {
const parsedStripped = parseJson(stripped);
if (parsedStripped.ok && parsedStripped.value && typeof parsedStripped.value === "object") {
if (
parsedStripped.ok &&
parsedStripped.value &&
typeof parsedStripped.value === 'object'
) {
return parsedStripped.value;
}
throw new Error(`JSON parse failed after stripping fences: ${parsedStripped.error}`);
throw new Error(
`JSON parse failed after stripping fences: ${parsedStripped.error}`,
);
}
throw new Error(`JSON parse failed: ${parsed.error}`);
@ -271,7 +278,7 @@ function config() {
ensureEnvLoaded();
const baseUrl = process.env.AI_PROXY_BASE_URL || "https://flatlogic.com";
const baseUrl = process.env.AI_PROXY_BASE_URL || 'https://flatlogic.com';
const projectId = process.env.PROJECT_ID || null;
let responsesPath = process.env.AI_RESPONSES_PATH || null;
if (!responsesPath && projectId) {
@ -286,8 +293,8 @@ function config() {
responsesPath,
projectId,
projectUuid: process.env.PROJECT_UUID || null,
projectHeader: process.env.AI_PROJECT_HEADER || "project-uuid",
defaultModel: process.env.AI_DEFAULT_MODEL || "gpt-5-mini",
projectHeader: process.env.AI_PROJECT_HEADER || 'project-uuid',
defaultModel: process.env.AI_DEFAULT_MODEL || 'gpt-5-mini',
timeout,
verifyTls,
};
@ -296,29 +303,38 @@ function config() {
}
function buildUrl(pathValue, baseUrl) {
const trimmed = String(pathValue || "").trim();
if (trimmed === "") {
const trimmed = String(pathValue || '').trim();
if (trimmed === '') {
return baseUrl;
}
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
return trimmed;
}
if (trimmed.startsWith("/")) {
if (trimmed.startsWith('/')) {
return `${baseUrl}${trimmed}`;
}
return `${baseUrl}/${trimmed}`;
}
function resolveStatusPath(aiRequestId, cfg) {
const basePath = (cfg.responsesPath || "").replace(/\/+$/, "");
const basePath = (cfg.responsesPath || '').replace(/\/+$/, '');
if (!basePath) {
return `/ai-request/${encodeURIComponent(String(aiRequestId))}/status`;
}
const normalized = basePath.endsWith("/ai-request") ? basePath : `${basePath}/ai-request`;
const normalized = basePath.endsWith('/ai-request')
? basePath
: `${basePath}/ai-request`;
return `${normalized}/${encodeURIComponent(String(aiRequestId))}/status`;
}
function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls) {
function sendRequest(
urlString,
method,
body,
headers,
timeoutSeconds,
verifyTls,
) {
return new Promise((resolve) => {
let targetUrl;
try {
@ -326,13 +342,13 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls
} catch (err) {
resolve({
success: false,
error: "invalid_url",
error: 'invalid_url',
message: err.message,
});
return;
}
const isHttps = targetUrl.protocol === "https:";
const isHttps = targetUrl.protocol === 'https:';
const requestFn = isHttps ? https.request : http.request;
const options = {
protocol: targetUrl.protocol,
@ -348,12 +364,12 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls
}
const req = requestFn(options, (res) => {
let responseBody = "";
res.setEncoding("utf8");
res.on("data", (chunk) => {
let responseBody = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
responseBody += chunk;
});
res.on("end", () => {
res.on('end', () => {
const status = res.statusCode || 0;
const parsed = parseJson(responseBody);
const payload = parsed.ok ? parsed.value : responseBody;
@ -372,9 +388,11 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls
}
const errorMessage =
parsed.ok && payload && typeof payload === "object"
? String(payload.error || payload.message || "AI proxy request failed")
: String(responseBody || "AI proxy request failed");
parsed.ok && payload && typeof payload === 'object'
? String(
payload.error || payload.message || 'AI proxy request failed',
)
: String(responseBody || 'AI proxy request failed');
resolve({
success: false,
@ -386,14 +404,14 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls
});
});
req.on("timeout", () => {
req.destroy(new Error("request_timeout"));
req.on('timeout', () => {
req.destroy(new Error('request_timeout'));
});
req.on("error", (err) => {
req.on('error', (err) => {
resolve({
success: false,
error: "request_failed",
error: 'request_failed',
message: err.message,
});
});
@ -406,8 +424,8 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls
}
function parseJson(value) {
if (typeof value !== "string" || value.trim() === "") {
return { ok: false, error: "empty_response" };
if (typeof value !== 'string' || value.trim() === '') {
return { ok: false, error: 'empty_response' };
}
try {
return { ok: true, value: JSON.parse(value) };
@ -418,11 +436,14 @@ function parseJson(value) {
function stripJsonFence(text) {
const trimmed = text.trim();
if (trimmed.startsWith("```json")) {
return trimmed.replace(/^```json/, "").replace(/```$/, "").trim();
if (trimmed.startsWith('```json')) {
return trimmed
.replace(/^```json/, '')
.replace(/```$/, '')
.trim();
}
if (trimmed.startsWith("```")) {
return trimmed.replace(/^```/, "").replace(/```$/, "").trim();
if (trimmed.startsWith('```')) {
return trimmed.replace(/^```/, '').replace(/```$/, '').trim();
}
return text;
}
@ -436,7 +457,7 @@ function resolveVerifyTls(value, fallback) {
if (value === undefined || value === null) {
return Boolean(fallback);
}
return String(value).toLowerCase() !== "false" && String(value) !== "0";
return String(value).toLowerCase() !== 'false' && String(value) !== '0';
}
function ensureEnvLoaded() {
@ -444,29 +465,32 @@ function ensureEnvLoaded() {
return;
}
const envPath = path.resolve(__dirname, "../../../../.env");
const envPath = path.resolve(__dirname, '../../../../.env');
if (!fs.existsSync(envPath)) {
return;
}
let content;
try {
content = fs.readFileSync(envPath, "utf8");
content = fs.readFileSync(envPath, 'utf8');
} catch (err) {
throw new Error(`Failed to read executor .env: ${err.message}`);
}
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) {
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
continue;
}
const [rawKey, ...rest] = trimmed.split("=");
const [rawKey, ...rest] = trimmed.split('=');
const key = rawKey.trim();
if (!key) {
continue;
}
const value = rest.join("=").trim().replace(/^['"]|['"]$/g, "");
const value = rest
.join('=')
.trim()
.replace(/^['"]|['"]$/g, '');
if (!process.env[key]) {
process.env[key] = value;
}

View File

@ -10,59 +10,68 @@ const GoogleStrategy = require('passport-google-oauth2').Strategy;
const MicrosoftStrategy = require('passport-microsoft').Strategy;
const UsersDBApi = require('../db/api/users');
passport.use(
new JWTstrategy(
{
passReqToCallback: true,
secretOrKey: config.secret_key,
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
},
async (req, token, done) => {
try {
const user = await UsersDBApi.findBy({ email: token.user.email });
passport.use(new JWTstrategy({
passReqToCallback: true,
secretOrKey: config.secret_key,
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken()
}, async (req, token, done) => {
try {
const user = await UsersDBApi.findBy( {email: token.user.email});
if (user && user.disabled) {
return done(new Error(`User '${user.email}' is disabled`));
}
if (user && user.disabled) {
return done (new Error(`User '${user.email}' is disabled`));
}
req.currentUser = user;
req.currentUser = user;
return done(null, user);
} catch (error) {
done(error);
}
},
),
);
return done(null, user);
} catch (error) {
done(error);
}
}));
passport.use(
new GoogleStrategy(
{
clientID: config.google.clientId,
clientSecret: config.google.clientSecret,
callbackURL: config.apiUrl + '/auth/signin/google/callback',
passReqToCallback: true,
},
function (request, accessToken, refreshToken, profile, done) {
socialStrategy(profile.email, profile, providers.GOOGLE, done);
},
),
);
passport.use(new GoogleStrategy({
clientID: config.google.clientId,
clientSecret: config.google.clientSecret,
callbackURL: config.apiUrl + '/auth/signin/google/callback',
passReqToCallback: true
},
function (request, accessToken, refreshToken, profile, done) {
socialStrategy(profile.email, profile, providers.GOOGLE, done);
}
));
passport.use(new MicrosoftStrategy({
clientID: config.microsoft.clientId,
clientSecret: config.microsoft.clientSecret,
callbackURL: config.apiUrl + '/auth/signin/microsoft/callback',
passReqToCallback: true
},
function (request, accessToken, refreshToken, profile, done) {
const email = profile._json.mail || profile._json.userPrincipalName;
socialStrategy(email, profile, providers.MICROSOFT, done);
}
));
passport.use(
new MicrosoftStrategy(
{
clientID: config.microsoft.clientId,
clientSecret: config.microsoft.clientSecret,
callbackURL: config.apiUrl + '/auth/signin/microsoft/callback',
passReqToCallback: true,
},
function (request, accessToken, refreshToken, profile, done) {
const email = profile._json.mail || profile._json.userPrincipalName;
socialStrategy(email, profile, providers.MICROSOFT, done);
},
),
);
function socialStrategy(email, profile, provider, done) {
db.users.findOrCreate({where: {email, provider}}).then(([user]) => {
db.users.findOrCreate({ where: { email, provider } }).then(([user]) => {
const body = {
id: user.id,
email: user.email,
name: profile.displayName,
};
const token = helpers.jwtSign({user: body});
return done(null, {token});
const token = helpers.jwtSign({ user: body });
return done(null, { token });
});
}

View File

@ -8,8 +8,8 @@ validateEnv();
const config = {
gcloud: {
bucket: "fldemo-files",
hash: "afeefb9d49f5b7977577876b99532ac7"
bucket: 'fldemo-files',
hash: 'afeefb9d49f5b7977577876b99532ac7',
},
s3: {
bucket: process.env.AWS_S3_BUCKET || '',
@ -19,33 +19,33 @@ const config = {
prefix: process.env.AWS_S3_PREFIX || 'afeefb9d49f5b7977577876b99532ac7',
},
bcrypt: {
saltRounds: 12
saltRounds: 12,
},
admin_pass: process.env.ADMIN_PASS || "88dbeaf8",
user_pass: process.env.USER_PASS || "c3baadeda5c6",
admin_email: process.env.ADMIN_EMAIL || "admin@flatlogic.com",
admin_pass: process.env.ADMIN_PASS || '88dbeaf8',
user_pass: process.env.USER_PASS || 'c3baadeda5c6',
admin_email: process.env.ADMIN_EMAIL || 'admin@flatlogic.com',
providers: {
LOCAL: 'local',
GOOGLE: 'google',
MICROSOFT: 'microsoft'
MICROSOFT: 'microsoft',
},
secret_key: process.env.SECRET_KEY || '88dbeaf8-e906-405e-9e41-c3baadeda5c6',
remote: '',
port: process.env.NODE_ENV === "production" ? "" : "8080",
hostUI: process.env.NODE_ENV === "production" ? "" : "http://localhost",
portUI: process.env.NODE_ENV === "production" ? "" : "3000",
port: process.env.NODE_ENV === 'production' ? '' : '8080',
hostUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost',
portUI: process.env.NODE_ENV === 'production' ? '' : '3000',
portUIProd: process.env.NODE_ENV === "production" ? "" : ":3000",
portUIProd: process.env.NODE_ENV === 'production' ? '' : ':3000',
swaggerUI: process.env.NODE_ENV === "production" ? "" : "http://localhost",
swaggerPort: process.env.NODE_ENV === "production" ? "" : ":8080",
swaggerUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost',
swaggerPort: process.env.NODE_ENV === 'production' ? '' : ':8080',
google: {
clientId: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
},
microsoft: {
clientId: process.env.MS_CLIENT_ID || '',
clientSecret: process.env.MS_CLIENT_SECRET || '',
clientId: process.env.MS_CLIENT_ID || '',
clientSecret: process.env.MS_CLIENT_SECRET || '',
},
uploadDir: os.tmpdir(),
email: {
@ -58,26 +58,26 @@ const config = {
},
tls: {
rejectUnauthorized: process.env.EMAIL_TLS_REJECT_UNAUTHORIZED !== 'false',
}
},
},
roles: {
admin: 'Administrator',
user: 'Analytics Viewer',
user: 'Analytics Viewer',
},
project_uuid: '88dbeaf8-e906-405e-9e41-c3baadeda5c6',
flHost: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'dev_stage' ? 'https://flatlogic.com/projects' : 'http://localhost:3000/projects',
flHost:
process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'dev_stage'
? 'https://flatlogic.com/projects'
: 'http://localhost:3000/projects',
gpt_key: process.env.GPT_KEY || '',
};
config.host = process.env.NODE_ENV === "production" ? config.remote : "http://localhost";
config.host =
process.env.NODE_ENV === 'production' ? config.remote : 'http://localhost';
config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`;
config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`;
config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`;

View File

@ -27,7 +27,15 @@ class Access_logsDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'environment', 'path', 'ip_address', 'user_agent', 'accessed_at', 'createdAt'];
return [
'id',
'environment',
'path',
'ip_address',
'user_agent',
'accessed_at',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -42,10 +50,7 @@ class Access_logsDBApi extends GenericDBApi {
}
static get FIND_BY_INCLUDES() {
return [
{ association: 'project' },
{ association: 'user' },
];
return [{ association: 'project' }, { association: 'user' }];
}
static getFieldMapping(data) {
@ -71,30 +76,50 @@ class Access_logsDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: filter.project ? {
[Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.project
? {
[Op.or]: [
{
id: {
[Op.in]: filter.project
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.project
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
{
model: db.users,
as: 'user',
where: filter.user ? {
[Op.or]: [
{ id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } },
{
firstName: {
[Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.user
? {
[Op.or]: [
{
id: {
[Op.in]: filter.user
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
firstName: {
[Op.or]: filter.user
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -145,9 +170,10 @@ class Access_logsDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -27,7 +27,15 @@ class Asset_variantsDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'variant_type', 'cdn_url', 'width_px', 'height_px', 'size_mb', 'createdAt'];
return [
'id',
'variant_type',
'cdn_url',
'width_px',
'height_px',
'size_mb',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -35,9 +43,7 @@ class Asset_variantsDBApi extends GenericDBApi {
}
static get ASSOCIATIONS() {
return [
{ field: 'asset', setter: 'setAsset', isArray: false },
];
return [{ field: 'asset', setter: 'setAsset', isArray: false }];
}
static get FIND_BY_INCLUDES() {
@ -67,16 +73,26 @@ class Asset_variantsDBApi extends GenericDBApi {
{
model: db.assets,
as: 'asset',
where: filter.asset ? {
[Op.or]: [
{ id: { [Op.in]: filter.asset.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.asset.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.asset
? {
[Op.or]: [
{
id: {
[Op.in]: filter.asset
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.asset
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -127,9 +143,10 @@ class Asset_variantsDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -27,7 +27,17 @@ class AssetsDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'name', 'asset_type', 'type', 'cdn_url', 'storage_key', 'mime_type', 'size_mb', 'createdAt'];
return [
'id',
'name',
'asset_type',
'type',
'cdn_url',
'storage_key',
'mime_type',
'size_mb',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -35,9 +45,7 @@ class AssetsDBApi extends GenericDBApi {
}
static get ASSOCIATIONS() {
return [
{ field: 'project', setter: 'setProject', isArray: false },
];
return [{ field: 'project', setter: 'setProject', isArray: false }];
}
static get FIND_BY_INCLUDES() {
@ -49,7 +57,12 @@ class AssetsDBApi extends GenericDBApi {
static get RELATION_FILTERS() {
return [
{ filterKey: 'project', model: db.projects, as: 'project', searchField: 'name' },
{
filterKey: 'project',
model: db.projects,
as: 'project',
searchField: 'name',
},
];
}
@ -83,16 +96,26 @@ class AssetsDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: filter.project ? {
[Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.project
? {
[Op.or]: [
{
id: {
[Op.in]: filter.project
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.project
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -143,9 +166,10 @@ class AssetsDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -67,12 +67,15 @@ class GenericDBApi {
createdById: currentUser.id,
updatedById: currentUser.id,
},
{ transaction }
{ transaction },
);
for (const assoc of this.ASSOCIATIONS) {
if (data[assoc.field] !== undefined) {
await record[assoc.setter](data[assoc.field] || (assoc.isArray ? [] : null), { transaction });
await record[assoc.setter](
data[assoc.field] || (assoc.isArray ? [] : null),
{ transaction },
);
}
}
@ -161,9 +164,8 @@ class GenericDBApi {
static async findBy(where, options = {}) {
const transaction = options.transaction;
const include = options.include !== undefined
? options.include
: this.FIND_BY_INCLUDES;
const include =
options.include !== undefined ? options.include : this.FIND_BY_INCLUDES;
const record = await this.MODEL.findOne({
where,
@ -237,16 +239,27 @@ class GenericDBApi {
model: rel.model,
as: rel.as,
required: searchTerms.length > 0,
where: searchTerms.length > 0 ? {
[Op.or]: [
{ id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
rel.searchField ? {
[rel.searchField]: {
[Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
where:
searchTerms.length > 0
? {
[Op.or]: [
{
id: {
[Op.in]: searchTerms.map((term) => Utils.uuid(term)),
},
},
rel.searchField
? {
[rel.searchField]: {
[Op.or]: searchTerms.map((term) => ({
[Op.iLike]: `%${term}%`,
})),
},
}
: {},
],
}
} : {}
]
} : undefined
: undefined,
};
include = [relInclude, ...include];
}
@ -256,9 +269,10 @@ class GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -23,7 +23,14 @@ class Element_type_defaultsDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'element_type', 'name', 'sort_order', 'is_active', 'createdAt'];
return [
'id',
'element_type',
'name',
'sort_order',
'is_active',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -37,7 +44,8 @@ class Element_type_defaultsDBApi extends GenericDBApi {
name: data.name ?? null,
sort_order: data.sort_order ?? 0,
default_settings_json:
data.default_settings_json === null || data.default_settings_json === undefined
data.default_settings_json === null ||
data.default_settings_json === undefined
? null
: typeof data.default_settings_json === 'string'
? data.default_settings_json

View File

@ -3,16 +3,9 @@ const assert = require('assert');
const services = require('../../services/file');
module.exports = class FileDBApi {
static async replaceRelationFiles(
relation,
rawFiles,
options,
) {
static async replaceRelationFiles(relation, rawFiles, options) {
assert(relation.belongsTo, 'belongsTo is required');
assert(
relation.belongsToColumn,
'belongsToColumn is required',
);
assert(relation.belongsToColumn, 'belongsToColumn is required');
assert(relation.belongsToId, 'belongsToId is required');
let files = [];
@ -29,11 +22,9 @@ module.exports = class FileDBApi {
static async _addFiles(relation, files, options) {
const transaction = (options && options.transaction) || undefined;
const currentUser = (options && options.currentUser) || {id: null};
const currentUser = (options && options.currentUser) || { id: null };
const inexistentFiles = files.filter(
(file) => !!file.new,
);
const inexistentFiles = files.filter((file) => !!file.new);
for (const file of inexistentFiles) {
await db.file.create(
@ -55,11 +46,7 @@ module.exports = class FileDBApi {
}
}
static async _removeLegacyFiles(
relation,
files,
options,
) {
static async _removeLegacyFiles(relation, files, options) {
const transaction = (options && options.transaction) || undefined;
const filesToDelete = await db.file.findAll({
@ -68,10 +55,9 @@ module.exports = class FileDBApi {
belongsToId: relation.belongsToId,
belongsToColumn: relation.belongsToColumn,
id: {
[db.Sequelize.Op
.notIn]: files
[db.Sequelize.Op.notIn]: files
.filter((file) => !file.new)
.map((file) => file.id)
.map((file) => file.id),
},
},
transaction,

View File

@ -27,7 +27,15 @@ class Presigned_url_requestsDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'purpose', 'asset_type', 'requested_key', 'mime_type', 'status', 'createdAt'];
return [
'id',
'purpose',
'asset_type',
'requested_key',
'mime_type',
'status',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -42,10 +50,7 @@ class Presigned_url_requestsDBApi extends GenericDBApi {
}
static get FIND_BY_INCLUDES() {
return [
{ association: 'project' },
{ association: 'user' },
];
return [{ association: 'project' }, { association: 'user' }];
}
static getFieldMapping(data) {
@ -73,30 +78,50 @@ class Presigned_url_requestsDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: filter.project ? {
[Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.project
? {
[Op.or]: [
{
id: {
[Op.in]: filter.project
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.project
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
{
model: db.users,
as: 'user',
where: filter.user ? {
[Op.or]: [
{ id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } },
{
firstName: {
[Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.user
? {
[Op.or]: [
{
id: {
[Op.in]: filter.user
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
firstName: {
[Op.or]: filter.user
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -147,9 +172,10 @@ class Presigned_url_requestsDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -31,7 +31,17 @@ class Project_audio_tracksDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'environment', 'source_key', 'name', 'slug', 'url', 'loop', 'volume', 'createdAt'];
return [
'id',
'environment',
'source_key',
'name',
'slug',
'url',
'loop',
'volume',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -39,9 +49,7 @@ class Project_audio_tracksDBApi extends GenericDBApi {
}
static get ASSOCIATIONS() {
return [
{ field: 'project', setter: 'setProject', isArray: false },
];
return [{ field: 'project', setter: 'setProject', isArray: false }];
}
static getFieldMapping(data) {
@ -64,7 +72,7 @@ class Project_audio_tracksDBApi extends GenericDBApi {
const queryWhere = applyRuntimeEnvironment({ ...where }, options);
const projectInclude = applyRuntimeProjectFilter(
{ model: db.projects, as: 'project' },
options
options,
);
const record = await this.MODEL.findOne({
@ -89,16 +97,26 @@ class Project_audio_tracksDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: filter.project ? {
[Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.project
? {
[Op.or]: [
{
id: {
[Op.in]: filter.project
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.project
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -153,9 +171,10 @@ class Project_audio_tracksDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -27,26 +27,34 @@ class Project_element_defaultsDBApi extends GenericDBApi {
}
static get ASSOCIATIONS() {
return [
{ field: 'project', setter: 'setProject', isArray: false },
];
return [{ field: 'project', setter: 'setProject', isArray: false }];
}
static get RELATION_FILTERS() {
return [
{ filterKey: 'project', model: db.projects, as: 'project', searchField: 'name' },
{
filterKey: 'project',
model: db.projects,
as: 'project',
searchField: 'name',
},
];
}
static get FIND_ALL_INCLUDES() {
return [
{ association: 'project' },
{ association: 'source_element' },
];
return [{ association: 'project' }, { association: 'source_element' }];
}
static get CSV_FIELDS() {
return ['id', 'element_type', 'name', 'sort_order', 'projectId', 'snapshot_version', 'createdAt'];
return [
'id',
'element_type',
'name',
'sort_order',
'projectId',
'snapshot_version',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -90,16 +98,26 @@ class Project_element_defaultsDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: projectFilter ? {
[Op.or]: [
{ id: { [Op.in]: projectFilter.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: projectFilter.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: projectFilter
? {
[Op.or]: [
{
id: {
[Op.in]: projectFilter
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: projectFilter
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
{
model: db.element_type_defaults,
@ -151,9 +169,10 @@ class Project_element_defaultsDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['sort_order', 'asc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['sort_order', 'asc']],
transaction: options.transaction,
};
@ -223,7 +242,7 @@ class Project_element_defaultsDBApi extends GenericDBApi {
{
transaction: options.transaction,
returning: true,
}
},
);
return projectDefaults;
@ -253,7 +272,9 @@ class Project_element_defaultsDBApi extends GenericDBApi {
});
if (!globalDefault) {
throw new Error(`No global default found for element type: ${projectDefault.element_type}`);
throw new Error(
`No global default found for element type: ${projectDefault.element_type}`,
);
}
// Update with global settings and increment version
@ -270,7 +291,7 @@ class Project_element_defaultsDBApi extends GenericDBApi {
},
{
transaction: options.transaction,
}
},
);
return projectDefault.reload();
@ -309,15 +330,18 @@ class Project_element_defaultsDBApi extends GenericDBApi {
}
// Parse JSON settings for comparison
const projectSettings = typeof projectDefault.settings_json === 'string'
? JSON.parse(projectDefault.settings_json || '{}')
: projectDefault.settings_json || {};
const projectSettings =
typeof projectDefault.settings_json === 'string'
? JSON.parse(projectDefault.settings_json || '{}')
: projectDefault.settings_json || {};
const globalSettings = typeof globalDefault.default_settings_json === 'string'
? JSON.parse(globalDefault.default_settings_json || '{}')
: globalDefault.default_settings_json || {};
const globalSettings =
typeof globalDefault.default_settings_json === 'string'
? JSON.parse(globalDefault.default_settings_json || '{}')
: globalDefault.default_settings_json || {};
const isDifferent = JSON.stringify(projectSettings) !== JSON.stringify(globalSettings) ||
const isDifferent =
JSON.stringify(projectSettings) !== JSON.stringify(globalSettings) ||
projectDefault.name !== globalDefault.name ||
projectDefault.sort_order !== globalDefault.sort_order;

View File

@ -27,7 +27,14 @@ class Project_membershipsDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'access_level', 'is_active', 'invited_at', 'accepted_at', 'createdAt'];
return [
'id',
'access_level',
'is_active',
'invited_at',
'accepted_at',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -42,10 +49,7 @@ class Project_membershipsDBApi extends GenericDBApi {
}
static get FIND_BY_INCLUDES() {
return [
{ association: 'project' },
{ association: 'user' },
];
return [{ association: 'project' }, { association: 'user' }];
}
static getFieldMapping(data) {
@ -70,30 +74,50 @@ class Project_membershipsDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: filter.project ? {
[Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.project
? {
[Op.or]: [
{
id: {
[Op.in]: filter.project
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.project
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
{
model: db.users,
as: 'user',
where: filter.user ? {
[Op.or]: [
{ id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } },
{
firstName: {
[Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.user
? {
[Op.or]: [
{
id: {
[Op.in]: filter.user
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
firstName: {
[Op.or]: filter.user
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -138,9 +162,10 @@ class Project_membershipsDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -16,7 +16,17 @@ class ProjectsDBApi extends GenericDBApi {
}
static get SEARCHABLE_FIELDS() {
return ['name', 'slug', 'description', 'logo_url', 'favicon_url', 'og_image_url', 'theme_config_json', 'custom_css_json', 'cdn_base_url'];
return [
'name',
'slug',
'description',
'logo_url',
'favicon_url',
'og_image_url',
'theme_config_json',
'custom_css_json',
'cdn_base_url',
];
}
static get RANGE_FIELDS() {
@ -28,7 +38,15 @@ class ProjectsDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'name', 'slug', 'description', 'logo_url', 'cdn_base_url', 'createdAt'];
return [
'id',
'name',
'slug',
'description',
'logo_url',
'cdn_base_url',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -81,9 +99,8 @@ class ProjectsDBApi extends GenericDBApi {
queryWhere.slug = runtimeProjectSlug;
}
const include = options.include !== undefined
? options.include
: this.DEFAULT_INCLUDES;
const include =
options.include !== undefined ? options.include : this.DEFAULT_INCLUDES;
const record = await this.MODEL.findOne({
where: queryWhere,
@ -113,7 +130,10 @@ class ProjectsDBApi extends GenericDBApi {
});
} catch (error) {
// Log but don't fail project creation if snapshot fails
console.error('Failed to snapshot global element defaults to project:', error);
console.error(
'Failed to snapshot global element defaults to project:',
error,
);
}
return project;
@ -182,9 +202,10 @@ class ProjectsDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -19,7 +19,13 @@ class Publish_eventsDBApi extends GenericDBApi {
}
static get RANGE_FIELDS() {
return ['started_at', 'finished_at', 'pages_copied', 'transitions_copied', 'audios_copied'];
return [
'started_at',
'finished_at',
'pages_copied',
'transitions_copied',
'audios_copied',
];
}
static get ENUM_FIELDS() {
@ -27,7 +33,16 @@ class Publish_eventsDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'title', 'description', 'from_environment', 'to_environment', 'status', 'pages_copied', 'createdAt'];
return [
'id',
'title',
'description',
'from_environment',
'to_environment',
'status',
'pages_copied',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -42,10 +57,7 @@ class Publish_eventsDBApi extends GenericDBApi {
}
static get FIND_BY_INCLUDES() {
return [
{ association: 'project' },
{ association: 'user' },
];
return [{ association: 'project' }, { association: 'user' }];
}
static getFieldMapping(data) {
@ -77,30 +89,50 @@ class Publish_eventsDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: filter.project ? {
[Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.project
? {
[Op.or]: [
{
id: {
[Op.in]: filter.project
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.project
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
{
model: db.users,
as: 'user',
where: filter.user ? {
[Op.or]: [
{ id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } },
{
firstName: {
[Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.user
? {
[Op.or]: [
{
id: {
[Op.in]: filter.user
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
firstName: {
[Op.or]: filter.user
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -151,9 +183,10 @@ class Publish_eventsDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -27,7 +27,14 @@ class Pwa_cachesDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'environment', 'cache_version', 'is_active', 'generated_at', 'createdAt'];
return [
'id',
'environment',
'cache_version',
'is_active',
'generated_at',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -35,9 +42,7 @@ class Pwa_cachesDBApi extends GenericDBApi {
}
static get ASSOCIATIONS() {
return [
{ field: 'project', setter: 'setProject', isArray: false },
];
return [{ field: 'project', setter: 'setProject', isArray: false }];
}
static get FIND_BY_INCLUDES() {
@ -68,16 +73,26 @@ class Pwa_cachesDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: filter.project ? {
[Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.project
? {
[Op.or]: [
{
id: {
[Op.in]: filter.project
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.project
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -128,9 +143,10 @@ class Pwa_cachesDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -35,16 +35,11 @@ class RolesDBApi extends GenericDBApi {
}
static get ASSOCIATIONS() {
return [
{ field: 'permissions', setter: 'setPermissions', isArray: true },
];
return [{ field: 'permissions', setter: 'setPermissions', isArray: true }];
}
static get FIND_BY_INCLUDES() {
return [
{ association: 'users_app_role' },
{ association: 'permissions' },
];
return [{ association: 'users_app_role' }, { association: 'permissions' }];
}
static getFieldMapping(data) {
@ -92,16 +87,25 @@ class RolesDBApi extends GenericDBApi {
model: db.permissions,
as: 'permissions_filter',
required: searchTerms.length > 0,
where: searchTerms.length > 0 ? {
[Op.or]: [
{ id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
where:
searchTerms.length > 0
? {
[Op.or]: [
{
id: {
[Op.in]: searchTerms.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: searchTerms.map((term) => ({
[Op.iLike]: `%${term}%`,
})),
},
},
],
}
}
]
} : undefined
: undefined,
},
...include,
];
@ -121,9 +125,10 @@ class RolesDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

View File

@ -19,7 +19,15 @@ class Tour_pagesDBApi extends GenericDBApi {
}
static get SEARCHABLE_FIELDS() {
return ['source_key', 'name', 'slug', 'background_image_url', 'background_video_url', 'background_audio_url', 'ui_schema_json'];
return [
'source_key',
'name',
'slug',
'background_image_url',
'background_video_url',
'background_audio_url',
'ui_schema_json',
];
}
static get RANGE_FIELDS() {
@ -31,7 +39,15 @@ class Tour_pagesDBApi extends GenericDBApi {
}
static get CSV_FIELDS() {
return ['id', 'environment', 'source_key', 'name', 'slug', 'sort_order', 'createdAt'];
return [
'id',
'environment',
'source_key',
'name',
'slug',
'sort_order',
'createdAt',
];
}
static get AUTOCOMPLETE_FIELD() {
@ -39,9 +55,7 @@ class Tour_pagesDBApi extends GenericDBApi {
}
static get ASSOCIATIONS() {
return [
{ field: 'project', setter: 'setProject', isArray: false },
];
return [{ field: 'project', setter: 'setProject', isArray: false }];
}
static getFieldMapping(data) {
@ -74,7 +88,7 @@ class Tour_pagesDBApi extends GenericDBApi {
createdById: currentUser.id,
updatedById: currentUser.id,
},
{ transaction }
{ transaction },
);
await record.setProject(projectId, { transaction });
@ -96,9 +110,7 @@ class Tour_pagesDBApi extends GenericDBApi {
const record = await this.MODEL.findOne({
where: queryWhere,
transaction,
include: [
projectInclude,
],
include: [projectInclude],
});
if (!record) return null;
@ -117,16 +129,26 @@ class Tour_pagesDBApi extends GenericDBApi {
{
model: db.projects,
as: 'project',
where: filter.project ? {
[Op.or]: [
{ id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } },
{
name: {
[Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
}
},
]
} : {},
where: filter.project
? {
[Op.or]: [
{
id: {
[Op.in]: filter.project
.split('|')
.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: filter.project
.split('|')
.map((term) => ({ [Op.iLike]: `%${term}%` })),
},
},
],
}
: {},
},
];
@ -181,9 +203,10 @@ class Tour_pagesDBApi extends GenericDBApi {
where,
include,
distinct: true,
order: filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
order:
filter.field && filter.sort
? [[filter.field, filter.sort]]
: [['createdAt', 'desc']],
transaction: options.transaction,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
module.exports = {
production: {
dialect: 'postgres',
@ -35,5 +33,5 @@ module.exports = {
seederStorage: 'sequelize',
migrationStorage: 'sequelize',
migrationStorageTableName: 'SequelizeMeta',
}
},
};

View File

@ -11,14 +11,20 @@ module.exports = {
try {
// Helper to add FK constraint safely (checks if exists first)
const addForeignKey = async (tableName, columnName, references, onDelete = 'CASCADE', onUpdate = 'CASCADE') => {
const addForeignKey = async (
tableName,
columnName,
references,
onDelete = 'CASCADE',
onUpdate = 'CASCADE',
) => {
const constraintName = `${tableName}_${columnName}_fkey`;
// Check if constraint already exists
const [results] = await queryInterface.sequelize.query(
`SELECT constraint_name FROM information_schema.table_constraints
WHERE table_name = '${tableName}' AND constraint_name = '${constraintName}'`,
{ transaction }
{ transaction },
);
if (results.length === 0) {
@ -41,61 +47,175 @@ module.exports = {
};
// asset_variants -> assets
await addForeignKey('asset_variants', 'assetId', { table: 'assets', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'asset_variants',
'assetId',
{ table: 'assets', field: 'id' },
'CASCADE',
'CASCADE',
);
// page_elements -> tour_pages
await addForeignKey('page_elements', 'pageId', { table: 'tour_pages', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'page_elements',
'pageId',
{ table: 'tour_pages', field: 'id' },
'CASCADE',
'CASCADE',
);
// page_links -> tour_pages (from_page)
await addForeignKey('page_links', 'from_pageId', { table: 'tour_pages', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'page_links',
'from_pageId',
{ table: 'tour_pages', field: 'id' },
'CASCADE',
'CASCADE',
);
// page_links -> tour_pages (to_page)
await addForeignKey('page_links', 'to_pageId', { table: 'tour_pages', field: 'id' }, 'SET NULL', 'CASCADE');
await addForeignKey(
'page_links',
'to_pageId',
{ table: 'tour_pages', field: 'id' },
'SET NULL',
'CASCADE',
);
// page_links -> transitions
await addForeignKey('page_links', 'transitionId', { table: 'transitions', field: 'id' }, 'SET NULL', 'CASCADE');
await addForeignKey(
'page_links',
'transitionId',
{ table: 'transitions', field: 'id' },
'SET NULL',
'CASCADE',
);
// assets -> projects
await addForeignKey('assets', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'assets',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// tour_pages -> projects
await addForeignKey('tour_pages', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'tour_pages',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// transitions -> projects
await addForeignKey('transitions', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'transitions',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// project_memberships -> projects
await addForeignKey('project_memberships', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'project_memberships',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// project_memberships -> users
await addForeignKey('project_memberships', 'userId', { table: 'users', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'project_memberships',
'userId',
{ table: 'users', field: 'id' },
'CASCADE',
'CASCADE',
);
// presigned_url_requests -> projects
await addForeignKey('presigned_url_requests', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'presigned_url_requests',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// presigned_url_requests -> users
await addForeignKey('presigned_url_requests', 'userId', { table: 'users', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'presigned_url_requests',
'userId',
{ table: 'users', field: 'id' },
'CASCADE',
'CASCADE',
);
// project_audio_tracks -> projects
await addForeignKey('project_audio_tracks', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'project_audio_tracks',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// publish_events -> projects
await addForeignKey('publish_events', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'publish_events',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// publish_events -> users (SET NULL to preserve audit trail)
await addForeignKey('publish_events', 'userId', { table: 'users', field: 'id' }, 'SET NULL', 'CASCADE');
await addForeignKey(
'publish_events',
'userId',
{ table: 'users', field: 'id' },
'SET NULL',
'CASCADE',
);
// pwa_caches -> projects
await addForeignKey('pwa_caches', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'pwa_caches',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// access_logs -> projects
await addForeignKey('access_logs', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
await addForeignKey(
'access_logs',
'projectId',
{ table: 'projects', field: 'id' },
'CASCADE',
'CASCADE',
);
// access_logs -> users (SET NULL to preserve audit trail)
await addForeignKey('access_logs', 'userId', { table: 'users', field: 'id' }, 'SET NULL', 'CASCADE');
await addForeignKey(
'access_logs',
'userId',
{ table: 'users', field: 'id' },
'SET NULL',
'CASCADE',
);
// users -> roles (SET NULL so deleting role doesn't delete users)
await addForeignKey('users', 'app_roleId', { table: 'roles', field: 'id' }, 'SET NULL', 'CASCADE');
await addForeignKey(
'users',
'app_roleId',
{ table: 'roles', field: 'id' },
'SET NULL',
'CASCADE',
);
await transaction.commit();
console.log('All FK constraints added successfully');
@ -112,10 +232,14 @@ module.exports = {
const dropForeignKey = async (tableName, columnName) => {
const constraintName = `${tableName}_${columnName}_fkey`;
try {
await queryInterface.removeConstraint(tableName, constraintName, { transaction });
await queryInterface.removeConstraint(tableName, constraintName, {
transaction,
});
console.log(`Removed FK constraint: ${constraintName}`);
} catch (error) {
console.log(`FK constraint not found (may not exist): ${constraintName}`);
console.log(
`FK constraint not found (may not exist): ${constraintName}`,
);
}
};
@ -146,5 +270,5 @@ module.exports = {
await transaction.rollback();
throw error;
}
}
},
};

View File

@ -21,20 +21,26 @@ module.exports = {
const [results] = await queryInterface.sequelize.query(
`SELECT column_name FROM information_schema.columns
WHERE table_name = '${tableName}' AND column_name = '${columnName}'`,
{ transaction }
{ transaction },
);
if (results.length > 0) {
await queryInterface.removeColumn(tableName, columnName, { transaction });
await queryInterface.removeColumn(tableName, columnName, {
transaction,
});
console.log(`Removed column: ${tableName}.${columnName}`);
} else {
console.log(`Column does not exist (skipping): ${tableName}.${columnName}`);
console.log(
`Column does not exist (skipping): ${tableName}.${columnName}`,
);
}
};
// Remove is_deleted index from assets first (if exists)
try {
await queryInterface.removeIndex('assets', 'assets_is_deleted', { transaction });
await queryInterface.removeIndex('assets', 'assets_is_deleted', {
transaction,
});
console.log('Removed index: assets_is_deleted');
} catch (error) {
console.log('Index assets_is_deleted not found (may not exist)');
@ -61,16 +67,26 @@ module.exports = {
try {
// Re-add columns to assets table
await queryInterface.addColumn('assets', 'is_deleted', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
}, { transaction });
await queryInterface.addColumn(
'assets',
'is_deleted',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
{ transaction },
);
await queryInterface.addColumn('assets', 'deleted_at_time', {
type: Sequelize.DATE,
allowNull: true,
}, { transaction });
await queryInterface.addColumn(
'assets',
'deleted_at_time',
{
type: Sequelize.DATE,
allowNull: true,
},
{ transaction },
);
// Re-add index
await queryInterface.addIndex('assets', ['is_deleted'], {
@ -79,16 +95,26 @@ module.exports = {
});
// Re-add columns to projects table
await queryInterface.addColumn('projects', 'is_deleted', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
}, { transaction });
await queryInterface.addColumn(
'projects',
'is_deleted',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
{ transaction },
);
await queryInterface.addColumn('projects', 'deleted_at_time', {
type: Sequelize.DATE,
allowNull: true,
}, { transaction });
await queryInterface.addColumn(
'projects',
'deleted_at_time',
{
type: Sequelize.DATE,
allowNull: true,
},
{ transaction },
);
await transaction.commit();
console.log('Redundant deletion columns restored successfully');
@ -96,5 +122,5 @@ module.exports = {
await transaction.rollback();
throw error;
}
}
},
};

View File

@ -17,7 +17,7 @@ module.exports = {
WHERE table_schema = 'public'
AND table_name = 'ui_elements'
);`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (!tableExists[0]?.exists) {
@ -32,11 +32,13 @@ module.exports = {
WHERE table_schema = 'public'
AND table_name = 'element_type_defaults'
);`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (newTableExists[0]?.exists) {
console.log('Table element_type_defaults already exists, skipping rename');
console.log(
'Table element_type_defaults already exists, skipping rename',
);
return;
}
@ -57,17 +59,21 @@ module.exports = {
WHERE table_schema = 'public'
AND table_name = 'element_type_defaults'
);`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (!tableExists[0]?.exists) {
console.log('Table element_type_defaults does not exist, skipping rollback');
console.log(
'Table element_type_defaults does not exist, skipping rollback',
);
return;
}
// Rename table back
await queryInterface.renameTable('element_type_defaults', 'ui_elements');
console.log('Successfully rolled back: renamed element_type_defaults to ui_elements');
console.log(
'Successfully rolled back: renamed element_type_defaults to ui_elements',
);
},
};

View File

@ -26,13 +26,13 @@ module.exports = {
type: Sequelize.TEXT,
allowNull: true,
},
{ transaction }
{ transaction },
);
// Step 2: Copy ENUM values to TEXT column
await queryInterface.sequelize.query(
`UPDATE page_elements SET element_type_text = element_type::TEXT`,
{ transaction }
{ transaction },
);
// Step 3: Drop the old ENUM column
@ -45,7 +45,7 @@ module.exports = {
'page_elements',
'element_type_text',
'element_type',
{ transaction }
{ transaction },
);
// Step 5: Add NOT NULL constraint
@ -56,7 +56,7 @@ module.exports = {
type: Sequelize.TEXT,
allowNull: false,
},
{ transaction }
{ transaction },
);
// Step 6: Now map nav_button to specific navigation types (column is TEXT now)
@ -70,7 +70,7 @@ module.exports = {
OR content_json::jsonb->>'navType' = 'forward'
OR content_json::jsonb->>'navType' IS NULL
)`,
{ transaction }
{ transaction },
);
// Back navigation
@ -80,17 +80,19 @@ module.exports = {
WHERE element_type = 'nav_button'
AND content_json IS NOT NULL
AND content_json::jsonb->>'navType' = 'back'`,
{ transaction }
{ transaction },
);
// Step 7: Drop the old ENUM type if it exists
await queryInterface.sequelize.query(
`DROP TYPE IF EXISTS "enum_page_elements_element_type"`,
{ transaction }
{ transaction },
);
await transaction.commit();
console.log('Successfully converted element_type from ENUM to TEXT and mapped nav_button types');
console.log(
'Successfully converted element_type from ENUM to TEXT and mapped nav_button types',
);
} catch (error) {
await transaction.rollback();
throw error;
@ -106,17 +108,17 @@ module.exports = {
`UPDATE page_elements
SET element_type = 'nav_button'
WHERE element_type IN ('navigation_next', 'navigation_prev')`,
{ transaction }
{ transaction },
);
// Step 2: Drop any existing ENUM types that might conflict
await queryInterface.sequelize.query(
`DROP TYPE IF EXISTS "enum_page_elements_element_type" CASCADE`,
{ transaction }
{ transaction },
);
await queryInterface.sequelize.query(
`DROP TYPE IF EXISTS "enum_page_elements_element_type_enum" CASCADE`,
{ transaction }
{ transaction },
);
// Step 3: Create the ENUM type with original values
@ -132,19 +134,19 @@ module.exports = {
'video_player',
'popup'
)`,
{ transaction }
{ transaction },
);
// Step 4: Add ENUM column directly via raw SQL to avoid Sequelize creating another type
await queryInterface.sequelize.query(
`ALTER TABLE page_elements ADD COLUMN element_type_enum "enum_page_elements_element_type"`,
{ transaction }
{ transaction },
);
// Step 5: Copy TEXT values to ENUM column
await queryInterface.sequelize.query(
`UPDATE page_elements SET element_type_enum = element_type::"enum_page_elements_element_type"`,
{ transaction }
{ transaction },
);
// Step 6: Drop TEXT column
@ -157,13 +159,13 @@ module.exports = {
'page_elements',
'element_type_enum',
'element_type',
{ transaction }
{ transaction },
);
// Step 8: Add NOT NULL constraint
await queryInterface.sequelize.query(
`ALTER TABLE page_elements ALTER COLUMN element_type SET NOT NULL`,
{ transaction }
{ transaction },
);
await transaction.commit();

View File

@ -21,11 +21,13 @@ module.exports = {
WHERE table_schema = 'public'
AND table_name = 'project_element_defaults'
);`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (tableExists[0]?.exists) {
console.log('Table project_element_defaults already exists, skipping creation');
console.log(
'Table project_element_defaults already exists, skipping creation',
);
return;
}
@ -121,19 +123,31 @@ module.exports = {
name: 'project_element_defaults_projectId',
});
await queryInterface.addIndex('project_element_defaults', ['projectId', 'element_type'], {
name: 'project_element_defaults_projectId_element_type',
unique: true,
where: { deletedAt: null },
});
await queryInterface.addIndex(
'project_element_defaults',
['projectId', 'element_type'],
{
name: 'project_element_defaults_projectId_element_type',
unique: true,
where: { deletedAt: null },
},
);
await queryInterface.addIndex('project_element_defaults', ['element_type'], {
name: 'project_element_defaults_element_type',
});
await queryInterface.addIndex(
'project_element_defaults',
['element_type'],
{
name: 'project_element_defaults_element_type',
},
);
await queryInterface.addIndex('project_element_defaults', ['source_element_id'], {
name: 'project_element_defaults_source_element_id',
});
await queryInterface.addIndex(
'project_element_defaults',
['source_element_id'],
{
name: 'project_element_defaults_source_element_id',
},
);
await queryInterface.addIndex('project_element_defaults', ['deletedAt'], {
name: 'project_element_defaults_deletedAt',
@ -144,11 +158,26 @@ module.exports = {
async down(queryInterface, _Sequelize) {
// Drop indexes first
await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_projectId');
await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_projectId_element_type');
await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_element_type');
await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_source_element_id');
await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_deletedAt');
await queryInterface.removeIndex(
'project_element_defaults',
'project_element_defaults_projectId',
);
await queryInterface.removeIndex(
'project_element_defaults',
'project_element_defaults_projectId_element_type',
);
await queryInterface.removeIndex(
'project_element_defaults',
'project_element_defaults_element_type',
);
await queryInterface.removeIndex(
'project_element_defaults',
'project_element_defaults_source_element_id',
);
await queryInterface.removeIndex(
'project_element_defaults',
'project_element_defaults_deletedAt',
);
// Drop table
await queryInterface.dropTable('project_element_defaults');

View File

@ -130,7 +130,7 @@ module.exports = {
// This is needed because the API's lazy initialization won't have run yet during migration
const [existingTypes] = await queryInterface.sequelize.query(
`SELECT element_type FROM element_type_defaults WHERE "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
const existingTypeSet = new Set(
@ -138,7 +138,7 @@ module.exports = {
? existingTypes.map((t) => t.element_type)
: existingTypes
? [existingTypes.element_type]
: []
: [],
);
// Insert missing element types
@ -154,16 +154,18 @@ module.exports = {
sort_order: defaultType.sort_order,
settings_json: defaultType.settings_json,
},
}
},
);
console.log(
`Created missing element_type_default: ${defaultType.element_type}`,
);
console.log(`Created missing element_type_default: ${defaultType.element_type}`);
}
}
// Get all existing projects
const [projects] = await queryInterface.sequelize.query(
`SELECT id FROM projects WHERE "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (!projects || projects.length === 0) {
@ -176,7 +178,7 @@ module.exports = {
`SELECT id, element_type, name, sort_order, settings_json
FROM element_type_defaults
WHERE "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (!globalDefaults || globalDefaults.length === 0) {
@ -184,8 +186,12 @@ module.exports = {
return;
}
const projectIds = Array.isArray(projects) ? projects.map((p) => p.id) : [projects.id];
const globalDefaultRows = Array.isArray(globalDefaults) ? globalDefaults : [globalDefaults];
const projectIds = Array.isArray(projects)
? projects.map((p) => p.id)
: [projects.id];
const globalDefaultRows = Array.isArray(globalDefaults)
? globalDefaults
: [globalDefaults];
// For each project, add any missing element type defaults
for (const projectId of projectIds) {
@ -196,7 +202,7 @@ module.exports = {
{
replacements: { projectId },
type: Sequelize.QueryTypes.SELECT,
}
},
);
const existingProjectTypes = new Set(
@ -204,7 +210,7 @@ module.exports = {
? existingDefaults.map((d) => d.element_type)
: existingDefaults
? [existingDefaults.element_type]
: []
: [],
);
// Create project element defaults for missing types
@ -239,26 +245,30 @@ module.exports = {
projectId,
},
type: Sequelize.QueryTypes.INSERT,
}
},
);
addedCount++;
}
if (addedCount > 0) {
console.log(`Backfilled ${addedCount} element defaults for project ${projectId}`);
console.log(
`Backfilled ${addedCount} element defaults for project ${projectId}`,
);
} else {
console.log(`Project ${projectId} already has all element defaults`);
}
}
console.log('Successfully backfilled project_element_defaults for existing projects');
console.log(
'Successfully backfilled project_element_defaults for existing projects',
);
},
async down(queryInterface, _Sequelize) {
// Delete all project_element_defaults with snapshot_version = 1
// (only the ones we created during backfill)
await queryInterface.sequelize.query(
`DELETE FROM project_element_defaults WHERE snapshot_version = 1`
`DELETE FROM project_element_defaults WHERE snapshot_version = 1`,
);
console.log('Successfully removed backfilled project_element_defaults');

View File

@ -12,17 +12,19 @@ module.exports = {
const [columns] = await queryInterface.sequelize.query(
`SELECT column_name FROM information_schema.columns
WHERE table_name = 'project_audio_tracks' AND column_name = 'environment'`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (!columns) {
console.log('Column project_audio_tracks.environment does not exist, skipping');
console.log(
'Column project_audio_tracks.environment does not exist, skipping',
);
return;
}
// Set NULL values to 'dev'
await queryInterface.sequelize.query(
`UPDATE project_audio_tracks SET environment = 'dev' WHERE environment IS NULL`
`UPDATE project_audio_tracks SET environment = 'dev' WHERE environment IS NULL`,
);
// Alter column to NOT NULL with default
@ -32,7 +34,9 @@ module.exports = {
defaultValue: 'dev',
});
console.log('Successfully fixed project_audio_tracks.environment to NOT NULL with default dev');
console.log(
'Successfully fixed project_audio_tracks.environment to NOT NULL with default dev',
);
},
async down(queryInterface, Sequelize) {

View File

@ -12,7 +12,7 @@ module.exports = {
// Get all projects
const projects = await queryInterface.sequelize.query(
`SELECT id FROM projects WHERE "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (!projects || projects.length === 0) {
@ -27,7 +27,7 @@ module.exports = {
const [stageCheck] = await queryInterface.sequelize.query(
`SELECT COUNT(*)::int as count FROM tour_pages
WHERE "projectId" = '${projectId}' AND environment = 'stage' AND "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (stageCheck?.count > 0) {
@ -39,7 +39,7 @@ module.exports = {
const [devPageCount] = await queryInterface.sequelize.query(
`SELECT COUNT(*)::int as count FROM tour_pages
WHERE "projectId" = '${projectId}' AND environment = 'dev' AND "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
if (!devPageCount || devPageCount.count === 0) {
@ -188,19 +188,19 @@ module.exports = {
async down(queryInterface, _Sequelize) {
// Delete all stage content that has a source_key (meaning it was created by this migration)
await queryInterface.sequelize.query(
`DELETE FROM page_links WHERE "from_pageId" IN (SELECT id FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL)`
`DELETE FROM page_links WHERE "from_pageId" IN (SELECT id FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL)`,
);
await queryInterface.sequelize.query(
`DELETE FROM page_elements WHERE "pageId" IN (SELECT id FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL)`
`DELETE FROM page_elements WHERE "pageId" IN (SELECT id FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL)`,
);
await queryInterface.sequelize.query(
`DELETE FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL`
`DELETE FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL`,
);
await queryInterface.sequelize.query(
`DELETE FROM transitions WHERE environment = 'stage' AND source_key IS NOT NULL`
`DELETE FROM transitions WHERE environment = 'stage' AND source_key IS NOT NULL`,
);
await queryInterface.sequelize.query(
`DELETE FROM project_audio_tracks WHERE environment = 'stage' AND source_key IS NOT NULL`
`DELETE FROM project_audio_tracks WHERE environment = 'stage' AND source_key IS NOT NULL`,
);
console.log('Removed stage content created by migration');

View File

@ -13,12 +13,12 @@ module.exports = {
async up(queryInterface, _Sequelize) {
// Fix any NULL environments in tour_pages
await queryInterface.sequelize.query(
`UPDATE tour_pages SET environment = 'dev' WHERE environment IS NULL`
`UPDATE tour_pages SET environment = 'dev' WHERE environment IS NULL`,
);
// Fix any NULL environments in transitions
await queryInterface.sequelize.query(
`UPDATE transitions SET environment = 'dev' WHERE environment IS NULL`
`UPDATE transitions SET environment = 'dev' WHERE environment IS NULL`,
);
// Add NOT NULL constraint with default to tour_pages.environment

View File

@ -11,7 +11,7 @@ module.exports = {
// Drop the ENUM type
await queryInterface.sequelize.query(
`DROP TYPE IF EXISTS "enum_projects_phase";`
`DROP TYPE IF EXISTS "enum_projects_phase";`,
);
},

View File

@ -18,25 +18,26 @@ module.exports = {
// Get all tour pages with their ui_schema_json
const [tourPages] = await queryInterface.sequelize.query(
`SELECT id, "projectId", environment, slug, ui_schema_json FROM tour_pages WHERE ui_schema_json IS NOT NULL`,
{ transaction }
{ transaction },
);
// Build a lookup map: pageId -> { projectId, environment, slug }
const pageInfoById = new Map();
tourPages.forEach(page => {
tourPages.forEach((page) => {
pageInfoById.set(page.id, {
projectId: page.projectId,
environment: page.environment,
slug: page.slug
slug: page.slug,
});
});
// Process each page and convert targetPageId to targetPageSlug
for (const page of tourPages) {
try {
const uiSchema = typeof page.ui_schema_json === 'string'
? JSON.parse(page.ui_schema_json)
: page.ui_schema_json;
const uiSchema =
typeof page.ui_schema_json === 'string'
? JSON.parse(page.ui_schema_json)
: page.ui_schema_json;
if (!uiSchema || !Array.isArray(uiSchema.elements)) {
continue;
@ -44,14 +45,19 @@ module.exports = {
let hasChanges = false;
uiSchema.elements.forEach(element => {
uiSchema.elements.forEach((element) => {
// Convert targetPageId to targetPageSlug
if (element.targetPageId && typeof element.targetPageId === 'string') {
if (
element.targetPageId &&
typeof element.targetPageId === 'string'
) {
const targetPageInfo = pageInfoById.get(element.targetPageId);
if (targetPageInfo && targetPageInfo.slug) {
// Only convert if target page is in the same project and environment
if (targetPageInfo.projectId === page.projectId &&
targetPageInfo.environment === page.environment) {
if (
targetPageInfo.projectId === page.projectId &&
targetPageInfo.environment === page.environment
) {
element.targetPageSlug = targetPageInfo.slug;
delete element.targetPageId;
hasChanges = true;
@ -66,11 +72,11 @@ module.exports = {
{
replacements: {
json: JSON.stringify(uiSchema),
id: page.id
id: page.id,
},
type: Sequelize.QueryTypes.UPDATE,
transaction
}
transaction,
},
);
}
} catch (parseError) {
@ -80,7 +86,9 @@ module.exports = {
}
await transaction.commit();
console.log('Migration complete: Converted targetPageId to targetPageSlug');
console.log(
'Migration complete: Converted targetPageId to targetPageSlug',
);
} catch (error) {
await transaction.rollback();
throw error;
@ -94,12 +102,12 @@ module.exports = {
// Get all tour pages
const [tourPages] = await queryInterface.sequelize.query(
`SELECT id, "projectId", environment, slug, ui_schema_json FROM tour_pages WHERE ui_schema_json IS NOT NULL`,
{ transaction }
{ transaction },
);
// Build lookup: (projectId, environment, slug) -> pageId
const pageIdByKey = new Map();
tourPages.forEach(page => {
tourPages.forEach((page) => {
const key = `${page.projectId}:${page.environment}:${page.slug}`;
pageIdByKey.set(key, page.id);
});
@ -107,9 +115,10 @@ module.exports = {
// Process each page and convert targetPageSlug back to targetPageId
for (const page of tourPages) {
try {
const uiSchema = typeof page.ui_schema_json === 'string'
? JSON.parse(page.ui_schema_json)
: page.ui_schema_json;
const uiSchema =
typeof page.ui_schema_json === 'string'
? JSON.parse(page.ui_schema_json)
: page.ui_schema_json;
if (!uiSchema || !Array.isArray(uiSchema.elements)) {
continue;
@ -117,8 +126,11 @@ module.exports = {
let hasChanges = false;
uiSchema.elements.forEach(element => {
if (element.targetPageSlug && typeof element.targetPageSlug === 'string') {
uiSchema.elements.forEach((element) => {
if (
element.targetPageSlug &&
typeof element.targetPageSlug === 'string'
) {
const key = `${page.projectId}:${page.environment}:${element.targetPageSlug}`;
const targetPageId = pageIdByKey.get(key);
if (targetPageId) {
@ -135,11 +147,11 @@ module.exports = {
{
replacements: {
json: JSON.stringify(uiSchema),
id: page.id
id: page.id,
},
type: Sequelize.QueryTypes.UPDATE,
transaction
}
transaction,
},
);
}
} catch (parseError) {
@ -148,10 +160,12 @@ module.exports = {
}
await transaction.commit();
console.log('Rollback complete: Converted targetPageSlug back to targetPageId');
console.log(
'Rollback complete: Converted targetPageSlug back to targetPageId',
);
} catch (error) {
await transaction.rollback();
throw error;
}
}
},
};

View File

@ -10,12 +10,14 @@ module.exports = {
async up(queryInterface, _Sequelize) {
// Verify the table is empty before dropping
const [results] = await queryInterface.sequelize.query(
'SELECT COUNT(*) as count FROM page_elements'
'SELECT COUNT(*) as count FROM page_elements',
);
const count = parseInt(results[0].count, 10);
if (count > 0) {
throw new Error(`Cannot drop page_elements table: it contains ${count} records. Please migrate or delete them first.`);
throw new Error(
`Cannot drop page_elements table: it contains ${count} records. Please migrate or delete them first.`,
);
}
await queryInterface.dropTable('page_elements');
@ -94,5 +96,5 @@ module.exports = {
unique: true,
},
});
}
},
};

View File

@ -10,12 +10,14 @@ module.exports = {
async up(queryInterface, _Sequelize) {
// Verify the table is empty before dropping
const [results] = await queryInterface.sequelize.query(
'SELECT COUNT(*) as count FROM page_links'
'SELECT COUNT(*) as count FROM page_links',
);
const count = parseInt(results[0].count, 10);
if (count > 0) {
throw new Error(`Cannot drop page_links table: it contains ${count} records. Please migrate or delete them first.`);
throw new Error(
`Cannot drop page_links table: it contains ${count} records. Please migrate or delete them first.`,
);
}
await queryInterface.dropTable('page_links');
@ -102,5 +104,5 @@ module.exports = {
unique: true,
},
});
}
},
};

View File

@ -10,12 +10,14 @@ module.exports = {
async up(queryInterface, _Sequelize) {
// Verify the table is empty before dropping
const [results] = await queryInterface.sequelize.query(
'SELECT COUNT(*) as count FROM transitions'
'SELECT COUNT(*) as count FROM transitions',
);
const count = parseInt(results[0].count, 10);
if (count > 0) {
throw new Error(`Cannot drop transitions table: it contains ${count} records. Please migrate or delete them first.`);
throw new Error(
`Cannot drop transitions table: it contains ${count} records. Please migrate or delete them first.`,
);
}
await queryInterface.dropTable('transitions');
@ -99,5 +101,5 @@ module.exports = {
unique: true,
},
});
}
},
};

View File

@ -67,14 +67,16 @@ module.exports = {
{
replacements: { element_type: elementType.element_type },
type: Sequelize.QueryTypes.SELECT,
}
},
);
if (!existing) {
await queryInterface.bulkInsert('element_type_defaults', [elementType]);
console.log(`Added global default for: ${elementType.element_type}`);
} else {
console.log(`Global default already exists for: ${elementType.element_type}`);
console.log(
`Global default already exists for: ${elementType.element_type}`,
);
}
}
@ -83,16 +85,18 @@ module.exports = {
`SELECT id, element_type, name, sort_order, settings_json
FROM element_type_defaults
WHERE element_type IN ('spot', 'logo', 'popup') AND "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
// Get all projects
const projects = await queryInterface.sequelize.query(
`SELECT id FROM projects WHERE "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT }
{ type: Sequelize.QueryTypes.SELECT },
);
console.log(`Backfilling ${globalDefaults.length} element types to ${projects.length} projects...`);
console.log(
`Backfilling ${globalDefaults.length} element types to ${projects.length} projects...`,
);
// Backfill project_element_defaults for each project
for (const project of projects) {
@ -102,24 +106,29 @@ module.exports = {
`SELECT id FROM project_element_defaults
WHERE "projectId" = :projectId AND element_type = :element_type AND "deletedAt" IS NULL`,
{
replacements: { projectId: project.id, element_type: globalDefault.element_type },
replacements: {
projectId: project.id,
element_type: globalDefault.element_type,
},
type: Sequelize.QueryTypes.SELECT,
}
},
);
if (!existing) {
await queryInterface.bulkInsert('project_element_defaults', [{
id: uuidv4(),
projectId: project.id,
element_type: globalDefault.element_type,
name: globalDefault.name,
sort_order: globalDefault.sort_order,
settings_json: globalDefault.settings_json,
source_element_id: globalDefault.id,
snapshot_version: 1,
createdAt: now,
updatedAt: now,
}]);
await queryInterface.bulkInsert('project_element_defaults', [
{
id: uuidv4(),
projectId: project.id,
element_type: globalDefault.element_type,
name: globalDefault.name,
sort_order: globalDefault.sort_order,
settings_json: globalDefault.settings_json,
source_element_id: globalDefault.id,
snapshot_version: 1,
createdAt: now,
updatedAt: now,
},
]);
}
}
}
@ -130,12 +139,12 @@ module.exports = {
async down(queryInterface, _Sequelize) {
// Remove the added element types from project_element_defaults
await queryInterface.sequelize.query(
`DELETE FROM project_element_defaults WHERE element_type IN ('spot', 'logo', 'popup')`
`DELETE FROM project_element_defaults WHERE element_type IN ('spot', 'logo', 'popup')`,
);
// Remove from element_type_defaults
await queryInterface.sequelize.query(
`DELETE FROM element_type_defaults WHERE element_type IN ('spot', 'logo', 'popup')`
`DELETE FROM element_type_defaults WHERE element_type IN ('spot', 'logo', 'popup')`,
);
},
};

View File

@ -0,0 +1,295 @@
'use strict';
const { v4: uuidv4 } = require('uuid');
/**
* Sync all 11 element type defaults with correct sort_order.
* This migration ensures all element types exist in element_type_defaults
* and backfills any missing project_element_defaults for existing projects.
*/
module.exports = {
async up(queryInterface, Sequelize) {
const now = new Date();
// Define all 11 element types with correct sort_order
const DEFAULT_ELEMENT_TYPES = [
{
element_type: 'navigation_next',
name: 'Navigation Forward Button',
sort_order: 1,
default_settings_json: {
label: 'Navigation: Forward',
navLabel: 'Forward',
navType: 'forward',
navDisabled: false,
transitionReverseMode: 'auto_reverse',
transitionDurationSec: 0.7,
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'navigation_prev',
name: 'Navigation Back Button',
sort_order: 2,
default_settings_json: {
label: 'Navigation: Back',
navLabel: 'Back',
navType: 'back',
navDisabled: false,
transitionReverseMode: 'auto_reverse',
transitionDurationSec: 0.7,
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'tooltip',
name: 'Tooltip',
sort_order: 3,
default_settings_json: {
label: 'Tooltip',
tooltipTitle: 'Tooltip title',
tooltipText: 'Tooltip text',
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'description',
name: 'Description',
sort_order: 4,
default_settings_json: {
label: 'Description',
descriptionTitle: 'TITLE',
descriptionText: '',
descriptionTitleFontSize: '48px',
descriptionTextFontSize: '36px',
descriptionTitleFontFamily: 'inherit',
descriptionTextFontFamily: 'inherit',
descriptionTitleColor: '#000000',
descriptionTextColor: '#4B5563',
descriptionBackgroundColor: 'transparent',
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'gallery',
name: 'Gallery',
sort_order: 5,
default_settings_json: {
label: 'Gallery',
galleryCards: [{ imageUrl: '', title: 'Card 1', description: '' }],
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'carousel',
name: 'Carousel',
sort_order: 6,
default_settings_json: {
label: 'Carousel',
carouselSlides: [{ imageUrl: '', caption: 'Slide 1' }],
carouselPrevIconUrl: '',
carouselNextIconUrl: '',
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'video_player',
name: 'Video Player',
sort_order: 7,
default_settings_json: {
label: 'Video Player',
mediaUrl: '',
mediaAutoplay: true,
mediaLoop: true,
mediaMuted: true,
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'audio_player',
name: 'Audio Player',
sort_order: 8,
default_settings_json: {
label: 'Audio Player',
mediaUrl: '',
mediaAutoplay: true,
mediaLoop: true,
mediaMuted: false,
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'spot',
name: 'Hotspot',
sort_order: 9,
default_settings_json: {
label: 'Hotspot',
iconUrl: '',
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'logo',
name: 'Logo',
sort_order: 10,
default_settings_json: {
label: 'Logo',
iconUrl: '',
backgroundImageUrl: '',
appearDelaySec: 0,
appearDurationSec: null,
},
},
{
element_type: 'popup',
name: 'Popup',
sort_order: 11,
default_settings_json: {
label: 'Popup',
iconUrl: '',
popupTitle: '',
popupContent: '',
appearDelaySec: 0,
appearDurationSec: null,
},
},
];
console.log('Syncing all 11 element type defaults...');
// Track inserted/updated global defaults for backfill
const globalDefaultIds = new Map();
// For each element type: insert if not exists, update sort_order if wrong
for (const elementType of DEFAULT_ELEMENT_TYPES) {
const [existing] = await queryInterface.sequelize.query(
`SELECT id, sort_order FROM element_type_defaults
WHERE element_type = :element_type AND "deletedAt" IS NULL`,
{
replacements: { element_type: elementType.element_type },
type: Sequelize.QueryTypes.SELECT,
},
);
if (!existing) {
// Insert new element type
const newId = uuidv4();
await queryInterface.bulkInsert('element_type_defaults', [
{
id: newId,
element_type: elementType.element_type,
name: elementType.name,
sort_order: elementType.sort_order,
settings_json: JSON.stringify(elementType.default_settings_json),
createdAt: now,
updatedAt: now,
},
]);
globalDefaultIds.set(elementType.element_type, newId);
console.log(
`Inserted: ${elementType.element_type} (sort_order: ${elementType.sort_order})`,
);
} else {
globalDefaultIds.set(elementType.element_type, existing.id);
// Update sort_order if different
if (existing.sort_order !== elementType.sort_order) {
await queryInterface.sequelize.query(
`UPDATE element_type_defaults
SET sort_order = :sort_order, "updatedAt" = :now
WHERE id = :id`,
{
replacements: {
sort_order: elementType.sort_order,
now,
id: existing.id,
},
},
);
console.log(
`Updated sort_order for ${elementType.element_type}: ${existing.sort_order} -> ${elementType.sort_order}`,
);
} else {
console.log(
`Already exists: ${elementType.element_type} (sort_order: ${elementType.sort_order})`,
);
}
}
}
// Get all projects
const projects = await queryInterface.sequelize.query(
`SELECT id FROM projects WHERE "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT },
);
console.log(
`Backfilling missing project_element_defaults for ${projects.length} projects...`,
);
// Get all global defaults for backfill
const globalDefaults = await queryInterface.sequelize.query(
`SELECT id, element_type, name, sort_order, settings_json
FROM element_type_defaults
WHERE "deletedAt" IS NULL`,
{ type: Sequelize.QueryTypes.SELECT },
);
let backfillCount = 0;
// Backfill project_element_defaults for each project
for (const project of projects) {
for (const globalDefault of globalDefaults) {
// Check if project already has this element type
const [existing] = await queryInterface.sequelize.query(
`SELECT id FROM project_element_defaults
WHERE "projectId" = :projectId AND element_type = :element_type AND "deletedAt" IS NULL`,
{
replacements: {
projectId: project.id,
element_type: globalDefault.element_type,
},
type: Sequelize.QueryTypes.SELECT,
},
);
if (!existing) {
await queryInterface.bulkInsert('project_element_defaults', [
{
id: uuidv4(),
projectId: project.id,
element_type: globalDefault.element_type,
name: globalDefault.name,
sort_order: globalDefault.sort_order,
settings_json: globalDefault.settings_json,
source_element_id: globalDefault.id,
snapshot_version: 1,
createdAt: now,
updatedAt: now,
},
]);
backfillCount++;
}
}
}
console.log(`Backfilled ${backfillCount} project element defaults.`);
console.log('Sync complete.');
},
async down(_queryInterface, _Sequelize) {
// This migration is safe - it only adds missing data
// No destructive down migration needed
console.log(
'No down migration needed - this migration only adds missing data.',
);
},
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const access_logs = sequelize.define(
'access_logs',
{
@ -8,50 +8,44 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
environment: {
environment: {
type: DataTypes.ENUM,
allowNull: false,
values: [
"admin",
"stage",
"production"
],
values: ['admin', 'stage', 'production'],
},
path: {
path: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 2048], msg: 'Path must be at most 2048 characters' },
},
},
ip_address: {
ip_address: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 45], msg: 'IP address must be at most 45 characters' },
len: {
args: [0, 45],
msg: 'IP address must be at most 45 characters',
},
},
},
user_agent: {
user_agent: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 1024], msg: 'User agent must be at most 1024 characters' },
len: {
args: [0, 1024],
msg: 'User agent must be at most 1024 characters',
},
},
},
accessed_at: {
accessed_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
importHash: {
@ -74,31 +68,9 @@ accessed_at: {
);
access_logs.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.access_logs.belongsTo(db.projects, {
as: 'project',
@ -120,9 +92,6 @@ accessed_at: {
onUpdate: 'CASCADE',
});
db.access_logs.belongsTo(db.users, {
as: 'createdBy',
});
@ -132,8 +101,5 @@ accessed_at: {
});
};
return access_logs;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const asset_variants = sequelize.define(
'asset_variants',
{
@ -8,38 +8,31 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
variant_type: {
variant_type: {
type: DataTypes.ENUM,
values: [
'thumbnail',
"thumbnail",
'preview',
'webp',
"preview",
'mp4_low',
'mp4_high',
"webp",
"mp4_low",
"mp4_high",
"original"
'original',
],
},
cdn_url: {
cdn_url: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 2048], msg: 'CDN URL must be at most 2048 characters' },
len: {
args: [0, 2048],
msg: 'CDN URL must be at most 2048 characters',
},
isUrlOrEmpty(value) {
if (value && value.length > 0 && !/^https?:\/\/.+/.test(value)) {
throw new Error('CDN URL must be a valid URL');
@ -48,21 +41,21 @@ cdn_url: {
},
},
width_px: {
width_px: {
type: DataTypes.INTEGER,
validate: {
min: { args: [0], msg: 'Width must be a non-negative integer' },
},
},
height_px: {
height_px: {
type: DataTypes.INTEGER,
validate: {
min: { args: [0], msg: 'Height must be a non-negative integer' },
},
},
size_mb: {
size_mb: {
type: DataTypes.DECIMAL,
validate: {
min: { args: [0], msg: 'Size must be a non-negative number' },
@ -83,31 +76,9 @@ size_mb: {
);
asset_variants.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.asset_variants.belongsTo(db.assets, {
as: 'asset',
@ -119,9 +90,6 @@ size_mb: {
onUpdate: 'CASCADE',
});
db.asset_variants.belongsTo(db.users, {
as: 'createdBy',
});
@ -131,9 +99,5 @@ size_mb: {
});
};
return asset_variants;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const assets = sequelize.define(
'assets',
{
@ -8,135 +8,92 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
name: {
name: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 255], msg: 'Asset name must be at most 255 characters' },
len: {
args: [0, 255],
msg: 'Asset name must be at most 255 characters',
},
},
},
asset_type: {
asset_type: {
type: DataTypes.ENUM,
allowNull: false,
values: [
"image",
"video",
"audio",
"file"
],
values: ['image', 'video', 'audio', 'file'],
},
type: {
type: {
type: DataTypes.ENUM,
allowNull: false,
defaultValue: "general",
defaultValue: 'general',
values: [
'icon',
"icon",
'background_image',
'audio',
"background_image",
'video',
'transition',
"audio",
'logo',
'favicon',
"video",
"transition",
"logo",
"favicon",
"document",
"general"
'document',
'general',
],
},
cdn_url: {
cdn_url: {
type: DataTypes.TEXT,
},
storage_key: {
storage_key: {
type: DataTypes.TEXT,
},
mime_type: {
mime_type: {
type: DataTypes.TEXT,
validate: {
is: { args: /^[a-z0-9]+\/[a-z0-9.+-]+$/i, msg: 'Invalid MIME type format' },
is: {
args: /^[a-z0-9]+\/[a-z0-9.+-]+$/i,
msg: 'Invalid MIME type format',
},
},
},
size_mb: {
size_mb: {
type: DataTypes.DECIMAL,
},
width_px: {
width_px: {
type: DataTypes.INTEGER,
},
height_px: {
height_px: {
type: DataTypes.INTEGER,
},
duration_sec: {
duration_sec: {
type: DataTypes.DECIMAL,
},
checksum: {
checksum: {
type: DataTypes.TEXT,
},
is_public: {
is_public: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
importHash: {
@ -160,41 +117,19 @@ is_public: {
);
assets.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
db.assets.hasMany(db.asset_variants, {
as: 'asset_variants_asset',
foreignKey: {
name: 'assetId',
name: 'assetId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
//end loop
//end loop
db.assets.belongsTo(db.projects, {
as: 'project',
@ -206,9 +141,6 @@ is_public: {
onUpdate: 'CASCADE',
});
db.assets.belongsTo(db.users, {
as: 'createdBy',
});
@ -218,7 +150,5 @@ is_public: {
});
};
return assets;
};

View File

@ -13,7 +13,10 @@ module.exports = function (sequelize, DataTypes) {
unique: true,
validate: {
notEmpty: { msg: 'Element type is required' },
len: { args: [1, 100], msg: 'Element type must be between 1 and 100 characters' },
len: {
args: [1, 100],
msg: 'Element type must be between 1 and 100 characters',
},
},
},
name: {
@ -21,7 +24,10 @@ module.exports = function (sequelize, DataTypes) {
allowNull: false,
validate: {
notEmpty: { msg: 'Name is required' },
len: { args: [1, 255], msg: 'Name must be between 1 and 255 characters' },
len: {
args: [1, 255],
msg: 'Name must be between 1 and 255 characters',
},
},
},
sort_order: {

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const file = sequelize.define(
'file',
{

View File

@ -5,7 +5,7 @@ const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require("../db.config")[env];
const config = require('../db.config')[env];
const db = {};
let sequelize;
@ -13,20 +13,29 @@ console.log(env);
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
sequelize = new Sequelize(
config.database,
config.username,
config.password,
config,
);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
fs.readdirSync(__dirname)
.filter((file) => {
return (
file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'
);
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes)
.forEach((file) => {
const model = require(path.join(__dirname, file))(
sequelize,
Sequelize.DataTypes,
);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
Object.keys(db).forEach((modelName) => {
if (db[modelName].associate) {
db[modelName].associate(db);
}

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const permissions = sequelize.define(
'permissions',
{
@ -8,13 +8,16 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
name: {
name: {
type: DataTypes.TEXT,
allowNull: false,
unique: true,
validate: {
notEmpty: { msg: 'Permission name is required' },
len: { args: [1, 100], msg: 'Permission name must be between 1 and 100 characters' },
len: {
args: [1, 100],
msg: 'Permission name must be between 1 and 100 characters',
},
},
},
@ -32,34 +35,9 @@ name: {
);
permissions.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.permissions.belongsTo(db.users, {
as: 'createdBy',
@ -70,9 +48,5 @@ name: {
});
};
return permissions;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const presigned_url_requests = sequelize.define(
'presigned_url_requests',
{
@ -8,82 +8,63 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
purpose: {
purpose: {
type: DataTypes.ENUM,
values: [
"upload",
"download"
],
values: ['upload', 'download'],
},
asset_type: {
asset_type: {
type: DataTypes.ENUM,
values: [
"image",
"video",
"audio",
"file"
],
values: ['image', 'video', 'audio', 'file'],
},
requested_key: {
requested_key: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 1024], msg: 'Requested key must be at most 1024 characters' },
len: {
args: [0, 1024],
msg: 'Requested key must be at most 1024 characters',
},
},
},
mime_type: {
mime_type: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 255], msg: 'MIME type must be at most 255 characters' },
len: {
args: [0, 255],
msg: 'MIME type must be at most 255 characters',
},
isMimeTypeOrEmpty(value) {
if (value && value.length > 0 && !/^[\w.-]+\/[\w.+-]+$/.test(value)) {
if (
value &&
value.length > 0 &&
!/^[\w.-]+\/[\w.+-]+$/.test(value)
) {
throw new Error('MIME type must be in format type/subtype');
}
},
},
},
requested_size_mb: {
requested_size_mb: {
type: DataTypes.DECIMAL,
validate: {
min: { args: [0], msg: 'Requested size must be a non-negative number' },
min: {
args: [0],
msg: 'Requested size must be a non-negative number',
},
},
},
expires_at: {
expires_at: {
type: DataTypes.DATE,
},
status: {
status: {
type: DataTypes.TEXT,
},
importHash: {
@ -100,31 +81,9 @@ status: {
);
presigned_url_requests.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.presigned_url_requests.belongsTo(db.projects, {
as: 'project',
@ -146,9 +105,6 @@ status: {
onUpdate: 'CASCADE',
});
db.presigned_url_requests.belongsTo(db.users, {
as: 'createdBy',
});
@ -158,9 +114,5 @@ status: {
});
};
return presigned_url_requests;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const project_audio_tracks = sequelize.define(
'project_audio_tracks',
{
@ -8,64 +8,42 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
environment: {
environment: {
type: DataTypes.ENUM,
values: [
"dev",
"stage",
"production"
],
values: ['dev', 'stage', 'production'],
},
source_key: {
source_key: {
type: DataTypes.TEXT,
},
name: {
name: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 255], msg: 'Audio track name must be at most 255 characters' },
len: {
args: [0, 255],
msg: 'Audio track name must be at most 255 characters',
},
},
},
slug: {
slug: {
type: DataTypes.TEXT,
},
url: {
url: {
type: DataTypes.TEXT,
},
loop: {
loop: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
volume: {
volume: {
type: DataTypes.DECIMAL,
validate: {
min: { args: [0], msg: 'Volume must be at least 0' },
@ -73,21 +51,15 @@ volume: {
},
},
sort_order: {
sort_order: {
type: DataTypes.INTEGER,
},
is_enabled: {
is_enabled: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
importHash: {
@ -104,31 +76,9 @@ is_enabled: {
);
project_audio_tracks.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.project_audio_tracks.belongsTo(db.projects, {
as: 'project',
@ -140,9 +90,6 @@ is_enabled: {
onUpdate: 'CASCADE',
});
db.project_audio_tracks.belongsTo(db.users, {
as: 'createdBy',
});
@ -152,9 +99,5 @@ is_enabled: {
});
};
return project_audio_tracks;
};

View File

@ -13,7 +13,10 @@ module.exports = function (sequelize, DataTypes) {
allowNull: false,
validate: {
notEmpty: { msg: 'Element type is required' },
len: { args: [1, 100], msg: 'Element type must be between 1 and 100 characters' },
len: {
args: [1, 100],
msg: 'Element type must be between 1 and 100 characters',
},
},
},
name: {

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const project_memberships = sequelize.define(
'project_memberships',
{
@ -8,50 +8,27 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
access_level: {
access_level: {
type: DataTypes.ENUM,
allowNull: false,
defaultValue: 'viewer',
values: [
"owner",
"editor",
"reviewer",
"viewer"
],
values: ['owner', 'editor', 'reviewer', 'viewer'],
},
is_active: {
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
invited_at: {
invited_at: {
type: DataTypes.DATE,
},
accepted_at: {
accepted_at: {
type: DataTypes.DATE,
},
importHash: {
@ -75,31 +52,9 @@ accepted_at: {
);
project_memberships.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.project_memberships.belongsTo(db.projects, {
as: 'project',
@ -121,9 +76,6 @@ accepted_at: {
onUpdate: 'CASCADE',
});
db.project_memberships.belongsTo(db.users, {
as: 'createdBy',
});
@ -133,8 +85,5 @@ accepted_at: {
});
};
return project_memberships;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const projects = sequelize.define(
'projects',
{
@ -8,67 +8,61 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
name: {
name: {
type: DataTypes.TEXT,
allowNull: false,
validate: {
notEmpty: { msg: 'Project name is required' },
len: { args: [1, 255], msg: 'Project name must be between 1 and 255 characters' },
len: {
args: [1, 255],
msg: 'Project name must be between 1 and 255 characters',
},
},
},
slug: {
slug: {
type: DataTypes.TEXT,
allowNull: false,
unique: true,
validate: {
notEmpty: { msg: 'Slug is required' },
is: { args: /^[a-z0-9_-]+$/i, msg: 'Slug can only contain letters, numbers, dashes, and underscores' },
len: { args: [1, 255], msg: 'Slug must be between 1 and 255 characters' },
is: {
args: /^[a-z0-9_-]+$/i,
msg: 'Slug can only contain letters, numbers, dashes, and underscores',
},
len: {
args: [1, 255],
msg: 'Slug must be between 1 and 255 characters',
},
},
},
description: {
description: {
type: DataTypes.TEXT,
},
logo_url: {
logo_url: {
type: DataTypes.TEXT,
},
favicon_url: {
favicon_url: {
type: DataTypes.TEXT,
},
og_image_url: {
og_image_url: {
type: DataTypes.TEXT,
},
theme_config_json: {
theme_config_json: {
type: DataTypes.JSON,
},
custom_css_json: {
custom_css_json: {
type: DataTypes.JSON,
},
cdn_base_url: {
cdn_base_url: {
type: DataTypes.TEXT,
},
importHash: {
@ -81,130 +75,104 @@ cdn_base_url: {
timestamps: true,
paranoid: true,
freezeTableName: true,
indexes: [
{ fields: ['slug'], unique: true },
{ fields: ['deletedAt'] },
],
indexes: [{ fields: ['slug'], unique: true }, { fields: ['deletedAt'] }],
},
);
projects.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
db.projects.hasMany(db.project_memberships, {
as: 'project_memberships_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.projects.hasMany(db.assets, {
as: 'assets_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.projects.hasMany(db.presigned_url_requests, {
as: 'presigned_url_requests_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.projects.hasMany(db.tour_pages, {
as: 'tour_pages_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.projects.hasMany(db.project_audio_tracks, {
as: 'project_audio_tracks_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.projects.hasMany(db.publish_events, {
as: 'publish_events_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.projects.hasMany(db.pwa_caches, {
as: 'pwa_caches_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.projects.hasMany(db.access_logs, {
as: 'access_logs_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.projects.hasMany(db.project_element_defaults, {
as: 'project_element_defaults_project',
foreignKey: {
name: 'projectId',
name: 'projectId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
//end loop
//end loop
db.projects.belongsTo(db.users, {
as: 'createdBy',
@ -215,8 +183,5 @@ cdn_base_url: {
});
};
return projects;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const publish_events = sequelize.define(
'publish_events',
{
@ -8,7 +8,7 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
title: {
title: {
type: DataTypes.STRING,
allowNull: true,
validate: {
@ -16,111 +16,78 @@ title: {
},
},
description: {
description: {
type: DataTypes.TEXT,
allowNull: true,
validate: {
len: { args: [0, 5000], msg: 'Description must be at most 5000 characters' },
len: {
args: [0, 5000],
msg: 'Description must be at most 5000 characters',
},
},
},
from_environment: {
from_environment: {
type: DataTypes.ENUM,
allowNull: false,
values: [
"dev",
"stage",
"production"
],
values: ['dev', 'stage', 'production'],
},
to_environment: {
to_environment: {
type: DataTypes.ENUM,
allowNull: false,
values: [
"dev",
"stage",
"production"
],
values: ['dev', 'stage', 'production'],
},
started_at: {
started_at: {
type: DataTypes.DATE,
},
finished_at: {
finished_at: {
type: DataTypes.DATE,
},
status: {
status: {
type: DataTypes.ENUM,
allowNull: false,
defaultValue: 'queued',
values: [
"queued",
"running",
"success",
"failed"
],
values: ['queued', 'running', 'success', 'failed'],
},
error_message: {
error_message: {
type: DataTypes.TEXT,
},
pages_copied: {
pages_copied: {
type: DataTypes.INTEGER,
validate: {
min: { args: [0], msg: 'Pages copied must be a non-negative integer' },
min: {
args: [0],
msg: 'Pages copied must be a non-negative integer',
},
},
},
transitions_copied: {
transitions_copied: {
type: DataTypes.INTEGER,
validate: {
min: { args: [0], msg: 'Transitions copied must be a non-negative integer' },
min: {
args: [0],
msg: 'Transitions copied must be a non-negative integer',
},
},
},
audios_copied: {
audios_copied: {
type: DataTypes.INTEGER,
validate: {
min: { args: [0], msg: 'Audios copied must be a non-negative integer' },
min: {
args: [0],
msg: 'Audios copied must be a non-negative integer',
},
},
},
@ -144,31 +111,9 @@ audios_copied: {
);
publish_events.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.publish_events.belongsTo(db.projects, {
as: 'project',
@ -190,9 +135,6 @@ audios_copied: {
onUpdate: 'CASCADE',
});
db.publish_events.belongsTo(db.users, {
as: 'createdBy',
});
@ -202,7 +144,5 @@ audios_copied: {
});
};
return publish_events;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const pwa_caches = sequelize.define(
'pwa_caches',
{
@ -8,55 +8,39 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
environment: {
environment: {
type: DataTypes.ENUM,
values: [
"dev",
"stage",
"production"
],
values: ['dev', 'stage', 'production'],
},
cache_version: {
cache_version: {
type: DataTypes.TEXT,
validate: {
len: { args: [0, 255], msg: 'Cache version must be at most 255 characters' },
len: {
args: [0, 255],
msg: 'Cache version must be at most 255 characters',
},
},
},
manifest_json: {
manifest_json: {
type: DataTypes.JSON,
},
asset_list_json: {
asset_list_json: {
type: DataTypes.JSON,
},
generated_at: {
generated_at: {
type: DataTypes.DATE,
},
is_active: {
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
importHash: {
@ -73,31 +57,9 @@ is_active: {
);
pwa_caches.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.pwa_caches.belongsTo(db.projects, {
as: 'project',
@ -109,9 +71,6 @@ is_active: {
onUpdate: 'CASCADE',
});
db.pwa_caches.belongsTo(db.users, {
as: 'createdBy',
});
@ -121,9 +80,5 @@ is_active: {
});
};
return pwa_caches;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const roles = sequelize.define(
'roles',
{
@ -8,20 +8,20 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
name: {
name: {
type: DataTypes.TEXT,
allowNull: false,
validate: {
notEmpty: { msg: 'Role name is required' },
len: { args: [1, 100], msg: 'Role name must be between 1 and 100 characters' },
len: {
args: [1, 100],
msg: 'Role name must be between 1 and 100 characters',
},
},
},
role_customization: {
role_customization: {
type: DataTypes.TEXT,
},
importHash: {
@ -38,7 +38,6 @@ role_customization: {
);
roles.associate = (db) => {
db.roles.belongsToMany(db.permissions, {
as: 'permissions',
foreignKey: {
@ -59,43 +58,19 @@ role_customization: {
through: 'rolesPermissionsPermissions',
});
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
db.roles.hasMany(db.users, {
as: 'users_app_role',
foreignKey: {
name: 'app_roleId',
name: 'app_roleId',
},
constraints: true,
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
});
//end loop
//end loop
db.roles.belongsTo(db.users, {
as: 'createdBy',
@ -106,9 +81,5 @@ role_customization: {
});
};
return roles;
};

View File

@ -1,4 +1,4 @@
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const tour_pages = sequelize.define(
'tour_pages',
{
@ -8,100 +8,79 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
environment: {
environment: {
type: DataTypes.ENUM,
allowNull: false,
defaultValue: 'dev',
values: [
"dev",
"stage",
"production"
],
values: ['dev', 'stage', 'production'],
},
source_key: {
source_key: {
type: DataTypes.TEXT,
},
name: {
name: {
type: DataTypes.TEXT,
allowNull: false,
validate: {
notEmpty: { msg: 'Page name is required' },
len: { args: [1, 255], msg: 'Page name must be between 1 and 255 characters' },
len: {
args: [1, 255],
msg: 'Page name must be between 1 and 255 characters',
},
},
},
slug: {
slug: {
type: DataTypes.TEXT,
allowNull: false,
validate: {
notEmpty: { msg: 'Slug is required' },
is: { args: /^[a-z0-9_-]+$/i, msg: 'Slug can only contain letters, numbers, dashes, and underscores' },
len: { args: [1, 255], msg: 'Slug must be between 1 and 255 characters' },
is: {
args: /^[a-z0-9_-]+$/i,
msg: 'Slug can only contain letters, numbers, dashes, and underscores',
},
len: {
args: [1, 255],
msg: 'Slug must be between 1 and 255 characters',
},
},
},
sort_order: {
sort_order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
background_image_url: {
background_image_url: {
type: DataTypes.TEXT,
},
background_video_url: {
background_video_url: {
type: DataTypes.TEXT,
},
background_audio_url: {
background_audio_url: {
type: DataTypes.TEXT,
},
background_loop: {
background_loop: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
requires_auth: {
requires_auth: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
ui_schema_json: {
ui_schema_json: {
type: DataTypes.JSON,
},
@ -125,31 +104,9 @@ ui_schema_json: {
);
tour_pages.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
//end loop
db.tour_pages.belongsTo(db.projects, {
as: 'project',
@ -161,9 +118,6 @@ ui_schema_json: {
onUpdate: 'CASCADE',
});
db.tour_pages.belongsTo(db.users, {
as: 'createdBy',
});
@ -173,8 +127,5 @@ ui_schema_json: {
});
};
return tour_pages;
};

View File

@ -3,7 +3,7 @@ const providers = config.providers;
const crypto = require('crypto');
const bcrypt = require('bcrypt');
module.exports = function(sequelize, DataTypes) {
module.exports = function (sequelize, DataTypes) {
const users = sequelize.define(
'users',
{
@ -13,28 +13,19 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
firstName: {
firstName: {
type: DataTypes.TEXT,
},
lastName: {
lastName: {
type: DataTypes.TEXT,
},
phoneNumber: {
phoneNumber: {
type: DataTypes.TEXT,
},
email: {
email: {
type: DataTypes.TEXT,
allowNull: false,
unique: true,
@ -44,65 +35,45 @@ email: {
},
},
disabled: {
disabled: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
password: {
password: {
type: DataTypes.TEXT,
allowNull: false,
},
emailVerified: {
emailVerified: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
emailVerificationToken: {
emailVerificationToken: {
type: DataTypes.TEXT,
},
emailVerificationTokenExpiresAt: {
emailVerificationTokenExpiresAt: {
type: DataTypes.DATE,
},
passwordResetToken: {
passwordResetToken: {
type: DataTypes.TEXT,
},
passwordResetTokenExpiresAt: {
passwordResetTokenExpiresAt: {
type: DataTypes.DATE,
},
provider: {
provider: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: providers.LOCAL,
},
importHash: {
@ -124,7 +95,6 @@ provider: {
);
users.associate = (db) => {
db.users.belongsToMany(db.permissions, {
as: 'custom_permissions',
foreignKey: {
@ -145,70 +115,49 @@ provider: {
through: 'usersCustom_permissionsPermissions',
});
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
db.users.hasMany(db.project_memberships, {
as: 'project_memberships_user',
foreignKey: {
name: 'userId',
name: 'userId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.users.hasMany(db.presigned_url_requests, {
as: 'presigned_url_requests_user',
foreignKey: {
name: 'userId',
name: 'userId',
},
constraints: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
db.users.hasMany(db.publish_events, {
as: 'publish_events_user',
foreignKey: {
name: 'userId',
name: 'userId',
},
constraints: true,
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
});
db.users.hasMany(db.access_logs, {
as: 'access_logs_user',
foreignKey: {
name: 'userId',
name: 'userId',
},
constraints: true,
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
});
//end loop
//end loop
db.users.belongsTo(db.roles, {
as: 'app_role',
@ -220,8 +169,6 @@ provider: {
onUpdate: 'CASCADE',
});
db.users.hasMany(db.file, {
as: 'avatar',
foreignKey: 'belongsToId',
@ -234,7 +181,6 @@ provider: {
},
});
db.users.belongsTo(db.users, {
as: 'createdBy',
});
@ -244,47 +190,41 @@ provider: {
});
};
users.beforeCreate((users) => {
users = trimStringFields(users);
users.beforeCreate((users) => {
users = trimStringFields(users);
if (
users.provider !== providers.LOCAL &&
Object.values(providers).indexOf(users.provider) > -1
) {
users.emailVerified = true;
if (users.provider !== providers.LOCAL && Object.values(providers).indexOf(users.provider) > -1) {
users.emailVerified = true;
if (!users.password) {
const password = crypto.randomBytes(20).toString('hex');
if (!users.password) {
const password = crypto
.randomBytes(20)
.toString('hex');
const hashedPassword = bcrypt.hashSync(
password,
config.bcrypt.saltRounds,
const hashedPassword = bcrypt.hashSync(
password,
config.bcrypt.saltRounds,
);
users.password = hashedPassword
}
}
});
users.password = hashedPassword;
}
}
});
users.beforeUpdate((users) => {
trimStringFields(users);
});
return users;
};
function trimStringFields(users) {
users.email = users.email.trim();
users.firstName = users.firstName
? users.firstName.trim()
: null;
users.firstName = users.firstName ? users.firstName.trim() : null;
users.lastName = users.lastName
? users.lastName.trim()
: null;
users.lastName = users.lastName ? users.lastName.trim() : null;
return users;
}

View File

@ -1,12 +1,12 @@
const db = require('./models');
const {execSync} = require("child_process");
const { execSync } = require('child_process');
console.log('Resetting Database');
db.sequelize
.sync({ force: true })
.then(() => {
execSync("sequelize db:seed:all");
execSync('sequelize db:seed:all');
console.log('OK');
process.exit();
})

View File

@ -1,51 +1,54 @@
'use strict';
const bcrypt = require("bcrypt");
const config = require("../../config");
const bcrypt = require('bcrypt');
const config = require('../../config');
const ids = [
'193bf4b5-9f07-4bd5-9a43-e7e41f3e96af',
'af5a87be-8f9c-4630-902a-37a60b7005ba',
'5bc531ab-611f-41f3-9373-b7cc5d09c93d',
]
'193bf4b5-9f07-4bd5-9a43-e7e41f3e96af',
'af5a87be-8f9c-4630-902a-37a60b7005ba',
'5bc531ab-611f-41f3-9373-b7cc5d09c93d',
];
module.exports = {
up: async (queryInterface) => {
let admin_hash = bcrypt.hashSync(config.admin_pass, config.bcrypt.saltRounds);
let admin_hash = bcrypt.hashSync(
config.admin_pass,
config.bcrypt.saltRounds,
);
let user_hash = bcrypt.hashSync(config.user_pass, config.bcrypt.saltRounds);
try {
await queryInterface.bulkInsert('users', [
{
id: ids[0],
firstName: 'Admin',
email: config.admin_email,
emailVerified: true,
provider: config.providers.LOCAL,
password: admin_hash,
createdAt: new Date(),
updatedAt: new Date()
},
{
id: ids[1],
firstName: 'John',
email: 'john@doe.com',
emailVerified: true,
provider: config.providers.LOCAL,
password: user_hash,
createdAt: new Date(),
updatedAt: new Date()
},
{
id: ids[2],
firstName: 'Client',
email: 'client@hello.com',
emailVerified: true,
provider: config.providers.LOCAL,
password: user_hash,
createdAt: new Date(),
updatedAt: new Date()
},
]);
await queryInterface.bulkInsert('users', [
{
id: ids[0],
firstName: 'Admin',
email: config.admin_email,
emailVerified: true,
provider: config.providers.LOCAL,
password: admin_hash,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: ids[1],
firstName: 'John',
email: 'john@doe.com',
emailVerified: true,
provider: config.providers.LOCAL,
password: user_hash,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: ids[2],
firstName: 'Client',
email: 'client@hello.com',
emailVerified: true,
provider: config.providers.LOCAL,
password: user_hash,
createdAt: new Date(),
updatedAt: new Date(),
},
]);
} catch (error) {
console.error('Error during bulkInsert:', error);
throw error;
@ -53,14 +56,18 @@ module.exports = {
},
down: async (queryInterface, Sequelize) => {
try {
await queryInterface.bulkDelete('users', {
id: {
[Sequelize.Op.in]: ids,
},
}, {});
} catch (error) {
console.error('Error during bulkDelete:', error);
throw error;
}
}
}
await queryInterface.bulkDelete(
'users',
{
id: {
[Sequelize.Op.in]: ids,
},
},
{},
);
} catch (error) {
console.error('Error during bulkDelete:', error);
throw error;
}
},
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,9 @@ const db = require('./models');
async function syncDatabase() {
if (process.env.NODE_ENV === 'production') {
console.error('ERROR: sync.js should not be run in production. Use migrations instead.');
console.error(
'ERROR: sync.js should not be run in production. Use migrations instead.',
);
process.exit(1);
}

View File

@ -15,10 +15,7 @@ module.exports = class Utils {
static ilike(model, column, value) {
return Sequelize.where(
Sequelize.fn(
'lower',
Sequelize.col(`${model}.${column}`),
),
Sequelize.fn('lower', Sequelize.col(`${model}.${column}`)),
{
[Sequelize.Op.like]: `%${value}%`.toLowerCase(),
},

View File

@ -9,82 +9,129 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) {
const permissionEntity = options.permissionEntity || entityName;
router.use(checkCrudPermissions(permissionEntity));
router.post('/', wrapAsync(async (req, res) => {
const referer = 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.host);
res.status(200).send(payload);
}));
router.post('/bulk-import', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await Service.bulkImport(req, res, true, link.host);
res.status(200).send(true);
}));
router.put('/:id', wrapAsync(async (req, res) => {
await Service.update(req.body.data, req.body.id, req.currentUser);
res.status(200).send(true);
}));
router.delete('/:id', wrapAsync(async (req, res) => {
await Service.remove(req.params.id, req.currentUser);
res.status(200).send(true);
}));
router.post('/deleteByIds', wrapAsync(async (req, res) => {
await Service.deleteByIds(req.body.data, req.currentUser);
res.status(200).send(true);
}));
router.get('/', wrapAsync(async (req, res) => {
const filetype = req.query.filetype;
const currentUser = req.currentUser;
const runtimeContext = req.runtimeContext;
const payload = await DBApi.findAll(req.query, { currentUser, runtimeContext });
if (filetype === 'csv') {
const fields = options.csvFields || DBApi.CSV_FIELDS || ['id', 'createdAt'];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment('export.csv').send(csv);
} catch (err) {
console.error(err);
res.status(500).send('CSV export error');
}
} else {
router.post(
'/',
wrapAsync(async (req, res) => {
const referer =
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.host,
);
res.status(200).send(payload);
}
}));
}),
);
router.get('/count', wrapAsync(async (req, res) => {
const currentUser = req.currentUser;
const runtimeContext = req.runtimeContext;
const payload = await DBApi.findAll(req.query, { countOnly: true, currentUser, runtimeContext });
res.status(200).send(payload);
}));
router.post(
'/bulk-import',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await Service.bulkImport(req, res, true, link.host);
res.status(200).send(true);
}),
);
router.get('/autocomplete', wrapAsync(async (req, res) => {
const payload = await DBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset
);
res.status(200).send(payload);
}));
router.put(
'/:id',
wrapAsync(async (req, res) => {
await Service.update(req.body.data, req.body.id, req.currentUser);
res.status(200).send(true);
}),
);
router.get('/:id', wrapAsync(async (req, res) => {
if (!isUuidV4(req.params.id)) {
return res.status(400).send(`Invalid ${entityName} id`);
}
router.delete(
'/:id',
wrapAsync(async (req, res) => {
await Service.remove(req.params.id, req.currentUser);
res.status(200).send(true);
}),
);
const runtimeContext = req.runtimeContext;
const payload = await DBApi.findBy({ id: req.params.id }, { runtimeContext });
res.status(200).send(payload);
}));
router.post(
'/deleteByIds',
wrapAsync(async (req, res) => {
await Service.deleteByIds(req.body.data, req.currentUser);
res.status(200).send(true);
}),
);
router.get(
'/',
wrapAsync(async (req, res) => {
const filetype = req.query.filetype;
const currentUser = req.currentUser;
const runtimeContext = req.runtimeContext;
const payload = await DBApi.findAll(req.query, {
currentUser,
runtimeContext,
});
if (filetype === 'csv') {
const fields = options.csvFields ||
DBApi.CSV_FIELDS || ['id', 'createdAt'];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment('export.csv').send(csv);
} catch (err) {
console.error(err);
res.status(500).send('CSV export error');
}
} else {
res.status(200).send(payload);
}
}),
);
router.get(
'/count',
wrapAsync(async (req, res) => {
const currentUser = req.currentUser;
const runtimeContext = req.runtimeContext;
const payload = await DBApi.findAll(req.query, {
countOnly: true,
currentUser,
runtimeContext,
});
res.status(200).send(payload);
}),
);
router.get(
'/autocomplete',
wrapAsync(async (req, res) => {
const payload = await DBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
);
res.status(200).send(payload);
}),
);
router.get(
'/:id',
wrapAsync(async (req, res) => {
if (!isUuidV4(req.params.id)) {
return res.status(400).send(`Invalid ${entityName} id`);
}
const runtimeContext = req.runtimeContext;
const payload = await DBApi.findBy(
{ id: req.params.id },
{ runtimeContext },
);
res.status(200).send(payload);
}),
);
if (options.customRoutes) {
options.customRoutes(router, Service, DBApi);

View File

@ -61,7 +61,10 @@ function createEntityService(DBApi, options = {}) {
throw new ValidationError(`${entityName}NotFound`);
}
const updated = await DBApi.update(id, data, { currentUser, transaction });
const updated = await DBApi.update(id, data, {
currentUser,
transaction,
});
await transaction.commit();
return updated;
} catch (error) {

View File

@ -20,10 +20,12 @@ module.exports = class Helpers {
}
static jwtSign(data) {
return jwt.sign(data, config.secret_key, {expiresIn: '6h'});
return jwt.sign(data, config.secret_key, { expiresIn: '6h' });
}
static isUuidV4(value) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
value,
);
}
};

View File

@ -18,8 +18,6 @@ const sqlRoutes = require('./routes/sql');
const openaiRoutes = require('./routes/openai');
const usersRoutes = require('./routes/users');
const rolesRoutes = require('./routes/roles');
@ -56,7 +54,6 @@ const {
sanitizePublicRuntimeListResponse,
} = require('./middlewares/runtime-public');
const getBaseUrl = (url) => {
if (!url) return '';
return url.endsWith('/api') ? url.slice(0, -4) : url;
@ -64,17 +61,18 @@ const getBaseUrl = (url) => {
const options = {
definition: {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "Tour Builder Platform",
description: "Tour Builder Platform Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.",
},
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Tour Builder Platform',
description:
'Tour Builder Platform Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.',
},
servers: [
{
url: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl,
description: "Development server",
}
description: 'Development server',
},
],
components: {
securitySchemes: {
@ -82,26 +80,34 @@ const options = {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
}
},
},
responses: {
UnauthorizedError: {
description: "Access token is missing or invalid"
}
}
description: 'Access token is missing or invalid',
},
},
},
security: [{
bearerAuth: []
}]
security: [
{
bearerAuth: [],
},
],
},
apis: ["./src/routes/*.js"],
apis: ['./src/routes/*.js'],
};
const specs = swaggerJsDoc(options);
app.use('/api-docs', function (req, res, next) {
swaggerUI.host = getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host');
next()
}, swaggerUI.serve, swaggerUI.setup(specs))
app.use(
'/api-docs',
function (req, res, next) {
swaggerUI.host =
getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host');
next();
},
swaggerUI.serve,
swaggerUI.setup(specs),
);
app.enable('trust proxy');
app.use(
@ -110,7 +116,7 @@ app.use(
crossOriginEmbedderPolicy: false,
}),
);
app.use(cors({origin: true}));
app.use(cors({ origin: true }));
require('./auth/auth');
// Request logger applied early so all routes are logged
@ -171,7 +177,6 @@ app.get('/api/health', async (req, res) => {
app.use('/api/auth', authRoutes);
app.use('/api/runtime-context', runtimeContextRoutes);
app.use('/api/users', jwtAuth, usersRoutes);
app.use('/api/roles', jwtAuth, rolesRoutes);
@ -200,7 +205,11 @@ app.use('/api/presigned_url_requests', jwtAuth, presigned_url_requestsRoutes);
mountRuntimeEntityRoute('/api/tour_pages', 'tour_pages', tour_pagesRoutes);
mountRuntimeEntityRoute('/api/project_audio_tracks', 'project_audio_tracks', project_audio_tracksRoutes);
mountRuntimeEntityRoute(
'/api/project_audio_tracks',
'project_audio_tracks',
project_audio_tracksRoutes,
);
app.use('/api/publish_events', jwtAuth, publish_eventsRoutes);
@ -210,43 +219,27 @@ app.use('/api/access_logs', jwtAuth, access_logsRoutes);
app.use('/api/element-type-defaults', jwtAuth, element_type_defaultsRoutes);
// Backwards compatibility alias for old API endpoint
app.use('/api/ui-elements', jwtAuth, element_type_defaultsRoutes);
app.use('/api/project-element-defaults', jwtAuth, project_element_defaultsRoutes);
app.use(
'/api/project-element-defaults',
jwtAuth,
project_element_defaultsRoutes,
);
app.use('/api/publish', jwtAuth, publishRoutes);
app.use(
'/api/openai',
jwtAuth,
openaiRoutes,
);
app.use(
'/api/ai',
jwtAuth,
openaiRoutes,
);
app.use('/api/openai', jwtAuth, openaiRoutes);
app.use('/api/ai', jwtAuth, openaiRoutes);
app.use(
'/api/search',
jwtAuth,
searchRoutes);
app.use(
'/api/sql',
jwtAuth,
sqlRoutes);
app.use('/api/search', jwtAuth, searchRoutes);
app.use('/api/sql', jwtAuth, sqlRoutes);
const publicDir = path.join(
__dirname,
'../public',
);
const publicDir = path.join(__dirname, '../public');
if (fs.existsSync(publicDir)) {
app.use('/', express.static(publicDir));
app.get('*', function(request, response) {
response.sendFile(
path.resolve(publicDir, 'index.html'),
);
app.get('*', function (request, response) {
response.sendFile(path.resolve(publicDir, 'index.html'));
});
}
@ -260,8 +253,11 @@ app.use((err, req, res, _next) => {
const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080;
app.listen(PORT, () => {
logger.info({ port: PORT, env: process.env.NODE_ENV || 'development' }, 'Server started');
});
app.listen(PORT, () => {
logger.info(
{ port: PORT, env: process.env.NODE_ENV || 'development' },
'Server started',
);
});
module.exports = app;

View File

@ -1,4 +1,3 @@
const ValidationError = require('../services/notifications/errors/validation');
const RolesDBApi = require('../db/api/roles');
@ -7,30 +6,38 @@ let publicRoleCache = null;
// Function to asynchronously fetch and cache the 'Public' role
async function fetchAndCachePublicRole() {
try {
// Use RolesDBApi to find the role by name 'Public'
publicRoleCache = await RolesDBApi.findBy({ name: 'Public' });
try {
// Use RolesDBApi to find the role by name 'Public'
publicRoleCache = await RolesDBApi.findBy({ name: 'Public' });
if (!publicRoleCache) {
console.error("WARNING: Role 'Public' not found in database during middleware startup. Check your migrations.");
// The system might not function correctly without this role. May need to throw an error or use a fallback stub.
} else {
console.log("'Public' role successfully loaded and cached.");
}
} catch (error) {
console.error("Error fetching 'Public' role during middleware startup:", error);
// Handle the error during startup fetch
throw error; // Important to know if the app can proceed without the Public role
if (!publicRoleCache) {
console.error(
"WARNING: Role 'Public' not found in database during middleware startup. Check your migrations.",
);
// The system might not function correctly without this role. May need to throw an error or use a fallback stub.
} else {
console.log("'Public' role successfully loaded and cached.");
}
} catch (error) {
console.error(
"Error fetching 'Public' role during middleware startup:",
error,
);
// Handle the error during startup fetch
throw error; // Important to know if the app can proceed without the Public role
}
}
// Trigger the role fetching when the check-permissions.js module is imported/loaded
// This should happen during application startup when routes are being configured.
fetchAndCachePublicRole().catch(error => {
// Handle the case where the fetchAndCachePublicRole promise is rejected
console.error("Critical error during permissions middleware initialization:", error);
// Decide here if the process should exit if the Public role is essential.
// process.exit(1);
fetchAndCachePublicRole().catch((error) => {
// Handle the case where the fetchAndCachePublicRole promise is rejected
console.error(
'Critical error during permissions middleware initialization:',
error,
);
// Decide here if the process should exit if the Public role is essential.
// process.exit(1);
});
/**
@ -39,85 +46,106 @@ fetchAndCachePublicRole().catch(error => {
* @return {import("express").RequestHandler} Express middleware function.
*/
function checkPermissions(permission) {
return async (req, res, next) => {
const { currentUser } = req;
return async (req, res, next) => {
const { currentUser } = req;
// 1. Check self-access bypass (only if the user is authenticated)
if (currentUser && (currentUser.id === req.params.id || currentUser.id === req.body.id)) {
return next(); // User has access to their own resource
}
// 1. Check self-access bypass (only if the user is authenticated)
if (
currentUser &&
(currentUser.id === req.params.id || currentUser.id === req.body.id)
) {
return next(); // User has access to their own resource
}
// 2. Check Custom Permissions (only if the user is authenticated)
if (currentUser) {
// Ensure custom_permissions is an array before using find
const customPermissions = Array.isArray(currentUser.custom_permissions)
? currentUser.custom_permissions
: [];
const userPermission = customPermissions.find(
(cp) => cp.name === permission,
// 2. Check Custom Permissions (only if the user is authenticated)
if (currentUser) {
// Ensure custom_permissions is an array before using find
const customPermissions = Array.isArray(currentUser.custom_permissions)
? currentUser.custom_permissions
: [];
const userPermission = customPermissions.find(
(cp) => cp.name === permission,
);
if (userPermission) {
return next(); // User has a custom permission
}
}
// 3. Determine the "effective" role for permission check
let effectiveRole = null;
try {
if (currentUser && currentUser.app_role) {
// User is authenticated and has an assigned role
effectiveRole = currentUser.app_role;
} else {
// User is NOT authenticated OR is authenticated but has no role
// Use the cached 'Public' role
if (!publicRoleCache) {
// If the cache is unexpectedly empty (e.g., startup error caught),
// we can try fetching the role again synchronously (less ideal) or just deny access.
console.error(
'Public role cache is empty. Attempting synchronous fetch...',
);
// Less efficient fallback option:
effectiveRole = await RolesDBApi.findBy({ name: 'Public' }); // Could be slow
if (!effectiveRole) {
// If even the synchronous attempt failed
return next(
new Error(
'Internal Server Error: Public role missing and cannot be fetched.',
),
);
if (userPermission) {
return next(); // User has a custom permission
}
}
} else {
effectiveRole = publicRoleCache; // Use the cached object
}
}
// 3. Determine the "effective" role for permission check
let effectiveRole = null;
try {
if (currentUser && currentUser.app_role) {
// User is authenticated and has an assigned role
effectiveRole = currentUser.app_role;
} else {
// User is NOT authenticated OR is authenticated but has no role
// Use the cached 'Public' role
if (!publicRoleCache) {
// If the cache is unexpectedly empty (e.g., startup error caught),
// we can try fetching the role again synchronously (less ideal) or just deny access.
console.error("Public role cache is empty. Attempting synchronous fetch...");
// Less efficient fallback option:
effectiveRole = await RolesDBApi.findBy({ name: 'Public' }); // Could be slow
if (!effectiveRole) {
// If even the synchronous attempt failed
return next(new Error("Internal Server Error: Public role missing and cannot be fetched."));
}
} else {
effectiveRole = publicRoleCache; // Use the cached object
}
}
// Check if we got a valid role object
if (!effectiveRole) {
return next(
new Error(
'Internal Server Error: Could not determine effective role.',
),
);
}
// Check if we got a valid role object
if (!effectiveRole) {
return next(new Error("Internal Server Error: Could not determine effective role."));
}
// 4. Check Permissions on the "effective" role
// Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method
// or a 'permissions' property (if permissions are eagerly loaded).
let rolePermissions = [];
if (typeof effectiveRole.getPermissions === 'function') {
rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists
} else if (Array.isArray(effectiveRole.permissions)) {
rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded
} else {
console.error(
'Role object lacks getPermissions() method or permissions property:',
effectiveRole,
);
return next(
new Error('Internal Server Error: Invalid role object format.'),
);
}
// 4. Check Permissions on the "effective" role
// Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method
// or a 'permissions' property (if permissions are eagerly loaded).
let rolePermissions = [];
if (typeof effectiveRole.getPermissions === 'function') {
rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists
} else if (Array.isArray(effectiveRole.permissions)) {
rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded
} else {
console.error("Role object lacks getPermissions() method or permissions property:", effectiveRole);
return next(new Error("Internal Server Error: Invalid role object format."));
}
if (rolePermissions.find((p) => p.name === permission)) {
next(); // The "effective" role has the required permission
} else {
// The "effective" role does not have the required permission
const roleName = effectiveRole.name || 'unknown role';
next(new ValidationError('auth.forbidden', `Role '${roleName}' denied access to '${permission}'.`));
}
} catch (e) {
// Handle errors during role or permission fetching
console.error("Error during permission check:", e);
next(e); // Pass the error to the next middleware
}
};
if (rolePermissions.find((p) => p.name === permission)) {
next(); // The "effective" role has the required permission
} else {
// The "effective" role does not have the required permission
const roleName = effectiveRole.name || 'unknown role';
next(
new ValidationError(
'auth.forbidden',
`Role '${roleName}' denied access to '${permission}'.`,
),
);
}
} catch (e) {
// Handle errors during role or permission fetching
console.error('Error during permission check:', e);
next(e); // Pass the error to the next middleware
}
};
}
const METHOD_MAP = {
@ -143,23 +171,24 @@ const RUNTIME_PUBLIC_READ_ENTITIES = new Set([
* @return {import("express").RequestHandler} Express middleware function.
*/
function checkCrudPermissions(name) {
return (req, res, next) => {
const isRuntimePublicRead = req.isRuntimePublicRequest === true
&& req.method === 'GET'
&& RUNTIME_PUBLIC_READ_ENTITIES.has(name.toUpperCase());
return (req, res, next) => {
const isRuntimePublicRead =
req.isRuntimePublicRequest === true &&
req.method === 'GET' &&
RUNTIME_PUBLIC_READ_ENTITIES.has(name.toUpperCase());
if (isRuntimePublicRead) {
return next();
}
if (isRuntimePublicRead) {
return next();
}
// Dynamically determine the permission name (e.g., 'READ_USERS')
const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`;
// Call the checkPermissions middleware with the determined permission
return checkPermissions(permissionName)(req, res, next);
};
// Dynamically determine the permission name (e.g., 'READ_USERS')
const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`;
// Call the checkPermissions middleware with the determined permission
return checkPermissions(permissionName)(req, res, next);
};
}
module.exports = {
checkPermissions,
checkCrudPermissions,
checkPermissions,
checkCrudPermissions,
};

View File

@ -12,7 +12,10 @@ function runtimeContextMiddleware(req, res, next) {
// Read environment from header (X-Runtime-Environment)
const headerEnvironment = req.headers['x-runtime-environment'];
if (headerEnvironment && ['production', 'stage', 'dev'].includes(headerEnvironment)) {
if (
headerEnvironment &&
['production', 'stage', 'dev'].includes(headerEnvironment)
) {
context.headerEnvironment = headerEnvironment;
}

View File

@ -49,7 +49,8 @@ const pickFields = (record, fields) => {
}
// Convert Sequelize instance to plain object if needed
const plainRecord = typeof record.get === 'function' ? record.get({ plain: true }) : record;
const plainRecord =
typeof record.get === 'function' ? record.get({ plain: true }) : record;
return fields.reduce((acc, field) => {
if (field in plainRecord && plainRecord[field] !== undefined) {
@ -83,7 +84,11 @@ const sanitizePublicRuntimeListResponse = (entityName) => {
const fields = PUBLIC_RUNTIME_ENTITY_FIELDS[entityName] || [];
return (req, res, next) => {
if (!isPublicRuntimeReadRequest(req) || req.path !== PUBLIC_RUNTIME_ALLOWED_PATH || fields.length === 0) {
if (
!isPublicRuntimeReadRequest(req) ||
req.path !== PUBLIC_RUNTIME_ALLOWED_PATH ||
fields.length === 0
) {
return next();
}

View File

@ -3,7 +3,7 @@ const Multer = require('multer');
let processFile = Multer({
storage: Multer.memoryStorage(),
}).single("file");
}).single('file');
let processFileMiddleware = util.promisify(processFile);
module.exports = processFileMiddleware;

View File

@ -5,7 +5,7 @@ const handleValidationErrors = (req, res, next) => {
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array().map(err => ({
details: errors.array().map((err) => ({
field: err.path,
message: err.msg,
value: err.value,
@ -24,48 +24,64 @@ const validators = {
requiredString: (field, min = 1, max = 255) =>
body(field)
.trim()
.notEmpty().withMessage(`${field} is required`)
.isLength({ min, max }).withMessage(`${field} must be ${min}-${max} characters`),
.notEmpty()
.withMessage(`${field} is required`)
.isLength({ min, max })
.withMessage(`${field} must be ${min}-${max} characters`),
optionalString: (field, max = 255) =>
body(field)
.optional()
.trim()
.isLength({ max }).withMessage(`${field} must be at most ${max} characters`),
.isLength({ max })
.withMessage(`${field} must be at most ${max} characters`),
slug: (field) =>
body(field)
.optional()
.trim()
.matches(/^[a-z0-9_-]+$/i).withMessage(`${field} can only contain letters, numbers, dashes, underscores`),
.matches(/^[a-z0-9_-]+$/i)
.withMessage(
`${field} can only contain letters, numbers, dashes, underscores`,
),
email: (field) =>
body(field)
.trim()
.isEmail().withMessage('Must be a valid email')
.isEmail()
.withMessage('Must be a valid email')
.normalizeEmail(),
optionalEmail: (field) =>
body(field)
.optional()
.trim()
.isEmail().withMessage('Must be a valid email')
.isEmail()
.withMessage('Must be a valid email')
.normalizeEmail(),
enum: (field, values) =>
body(field)
.optional()
.isIn(values).withMessage(`${field} must be one of: ${values.join(', ')}`),
.isIn(values)
.withMessage(`${field} must be one of: ${values.join(', ')}`),
boolean: (field) =>
body(field)
.optional()
.isBoolean().withMessage(`${field} must be a boolean`),
.isBoolean()
.withMessage(`${field} must be a boolean`),
integer: (field, min, max) => {
let validator = body(field).optional().isInt();
if (min !== undefined) validator = validator.custom(val => val >= min).withMessage(`${field} must be at least ${min}`);
if (max !== undefined) validator = validator.custom(val => val <= max).withMessage(`${field} must be at most ${max}`);
if (min !== undefined)
validator = validator
.custom((val) => val >= min)
.withMessage(`${field} must be at least ${min}`);
if (max !== undefined)
validator = validator
.custom((val) => val <= max)
.withMessage(`${field} must be at most ${max}`);
return validator;
},
@ -78,15 +94,20 @@ const validators = {
body(field)
.optional()
.trim()
.isURL().withMessage(`${field} must be a valid URL`),
.isURL()
.withMessage(`${field} must be a valid URL`),
};
function createEntityValidation(entityConfig = {}) {
const { fields = [], requiredFields = [] } = entityConfig;
const fieldValidators = fields.map(field => {
const fieldValidators = fields.map((field) => {
if (requiredFields.includes(field.name)) {
return validators.requiredString(`data.${field.name}`, field.min || 1, field.max || 255);
return validators.requiredString(
`data.${field.name}`,
field.min || 1,
field.max || 255,
);
}
return validators.optionalString(`data.${field.name}`, field.max || 255);
});

View File

@ -138,4 +138,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('access_logs', Access_logsService, Access_logsDBApi);
module.exports = createEntityRouter(
'access_logs',
Access_logsService,
Access_logsDBApi,
);

View File

@ -140,4 +140,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('asset_variants', Asset_variantsService, Asset_variantsDBApi);
module.exports = createEntityRouter(
'asset_variants',
Asset_variantsService,
Asset_variantsDBApi,
);

View File

@ -10,30 +10,28 @@ const wrapAsync = require('../helpers').wrapAsync;
const router = express.Router();
const authRateLimitStore = new Map();
const createMemoryRateLimiter = ({
keyPrefix,
maxRequests,
windowMs,
}) => (req, res, next) => {
const key = `${keyPrefix}:${req.ip || 'unknown'}`;
const now = Date.now();
const current = authRateLimitStore.get(key);
const createMemoryRateLimiter =
({ keyPrefix, maxRequests, windowMs }) =>
(req, res, next) => {
const key = `${keyPrefix}:${req.ip || 'unknown'}`;
const now = Date.now();
const current = authRateLimitStore.get(key);
if (!current || current.expiresAt <= now) {
authRateLimitStore.set(key, { count: 1, expiresAt: now + windowMs });
if (!current || current.expiresAt <= now) {
authRateLimitStore.set(key, { count: 1, expiresAt: now + windowMs });
return next();
}
if (current.count >= maxRequests) {
return res.status(429).send({
message: 'Too many requests. Please try again later.',
});
}
current.count += 1;
authRateLimitStore.set(key, current);
return next();
}
if (current.count >= maxRequests) {
return res.status(429).send({
message: 'Too many requests. Please try again later.',
});
}
current.count += 1;
authRateLimitStore.set(key, current);
return next();
};
};
const signinLimiter = createMemoryRateLimiter({
keyPrefix: 'signin',
@ -71,7 +69,9 @@ function safeParseUrl(value) {
function getRequestHost(req) {
const uiUrl = safeParseUrl(config.uiUrl);
const fallbackHost = uiUrl ? uiUrl.origin : config.backUrl || 'http://localhost:3000';
const fallbackHost = uiUrl
? uiUrl.origin
: config.backUrl || 'http://localhost:3000';
const origin = safeParseUrl(req.headers.origin);
const referer = safeParseUrl(req.headers.referer);
@ -142,10 +142,18 @@ function getRequestHost(req) {
* x-codegen-request-body-name: body
*/
router.post('/signin/local', signinLimiter, wrapAsync(async (req, res) => {
const payload = await AuthService.signin(req.body.email, req.body.password, req,);
res.status(200).send(payload);
}));
router.post(
'/signin/local',
signinLimiter,
wrapAsync(async (req, res) => {
const payload = await AuthService.signin(
req.body.email,
req.body.password,
req,
);
res.status(200).send(payload);
}),
);
/**
* @swagger
@ -164,42 +172,69 @@ router.post('/signin/local', signinLimiter, wrapAsync(async (req, res) => {
* x-codegen-request-body-name: body
*/
router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => {
if (!req.currentUser || !req.currentUser.id) {
throw new ForbiddenError();
}
router.get(
'/me',
passport.authenticate('jwt', { session: false }),
(req, res) => {
if (!req.currentUser || !req.currentUser.id) {
throw new ForbiddenError();
}
const payload = req.currentUser;
delete payload.password;
res.status(200).send(payload);
});
const payload = req.currentUser;
delete payload.password;
res.status(200).send(payload);
},
);
router.put('/password-reset', wrapAsync(async (req, res) => {
const payload = await AuthService.passwordReset(req.body.token, req.body.password, req,);
res.status(200).send(payload);
}));
router.put(
'/password-reset',
wrapAsync(async (req, res) => {
const payload = await AuthService.passwordReset(
req.body.token,
req.body.password,
req,
);
res.status(200).send(payload);
}),
);
router.put('/password-update', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
const payload = await AuthService.passwordUpdate(req.body.currentPassword, req.body.newPassword, req);
res.status(200).send(payload);
}));
router.put(
'/password-update',
passport.authenticate('jwt', { session: false }),
wrapAsync(async (req, res) => {
const payload = await AuthService.passwordUpdate(
req.body.currentPassword,
req.body.newPassword,
req,
);
res.status(200).send(payload);
}),
);
router.post('/send-email-address-verification-email', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
if (!req.currentUser) {
throw new ForbiddenError();
}
router.post(
'/send-email-address-verification-email',
passport.authenticate('jwt', { session: false }),
wrapAsync(async (req, res) => {
if (!req.currentUser) {
throw new ForbiddenError();
}
await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email);
const payload = true;
res.status(200).send(payload);
}));
await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email);
const payload = true;
res.status(200).send(payload);
}),
);
router.post('/send-password-reset-email', passwordResetLimiter, wrapAsync(async (req, res) => {
const host = getRequestHost(req);
await AuthService.sendPasswordResetEmail(req.body.email, 'register', host);
const payload = true;
res.status(200).send(payload);
}));
router.post(
'/send-password-reset-email',
passwordResetLimiter,
wrapAsync(async (req, res) => {
const host = getRequestHost(req);
await AuthService.sendPasswordResetEmail(req.body.email, 'register', host);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
@ -224,32 +259,47 @@ router.post('/send-password-reset-email', passwordResetLimiter, wrapAsync(async
* x-codegen-request-body-name: body
*/
router.post('/signup', signupLimiter, wrapAsync(async (req, res) => {
const host = getRequestHost(req);
const payload = await AuthService.signup(
router.post(
'/signup',
signupLimiter,
wrapAsync(async (req, res) => {
const host = getRequestHost(req);
const payload = await AuthService.signup(
req.body.email,
req.body.password,
req,
host,
)
res.status(200).send(payload);
}));
);
res.status(200).send(payload);
}),
);
router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
if (!req.currentUser || !req.currentUser.id) {
throw new ForbiddenError();
}
router.put(
'/profile',
passport.authenticate('jwt', { session: false }),
wrapAsync(async (req, res) => {
if (!req.currentUser || !req.currentUser.id) {
throw new ForbiddenError();
}
await AuthService.updateProfile(req.body.profile, req.currentUser);
const payload = true;
res.status(200).send(payload);
}));
await AuthService.updateProfile(req.body.profile, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
router.put('/verify-email', wrapAsync(async (req, res) => {
const payload = await AuthService.verifyEmail(req.body.token, req, req.headers.referer)
res.status(200).send(payload);
}));
router.put(
'/verify-email',
wrapAsync(async (req, res) => {
const payload = await AuthService.verifyEmail(
req.body.token,
req,
req.headers.referer,
);
res.status(200).send(payload);
}),
);
router.get('/email-configured', (req, res) => {
const payload = EmailSender.isConfigured;
@ -257,36 +307,46 @@ router.get('/email-configured', (req, res) => {
});
router.get('/signin/google', (req, res, next) => {
passport.authenticate("google", {scope: ["profile", "email"], state: req.query.app})(req, res, next);
});
router.get('/signin/google/callback', passport.authenticate("google", {failureRedirect: "/login", session: false}),
function (req, res) {
socialRedirect(res, req.query.state, req.user.token, config);
}
);
router.get('/signin/microsoft', (req, res, next) => {
passport.authenticate("microsoft", {
scope: ["https://graph.microsoft.com/user.read openid"],
state: req.query.app
passport.authenticate('google', {
scope: ['profile', 'email'],
state: req.query.app,
})(req, res, next);
});
router.get('/signin/microsoft/callback', passport.authenticate("microsoft", {
failureRedirect: "/login",
session: false
router.get(
'/signin/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
session: false,
}),
function (req, res) {
socialRedirect(res, req.query.state, req.user.token, config);
},
);
router.get('/signin/microsoft', (req, res, next) => {
passport.authenticate('microsoft', {
scope: ['https://graph.microsoft.com/user.read openid'],
state: req.query.app,
})(req, res, next);
});
router.get(
'/signin/microsoft/callback',
passport.authenticate('microsoft', {
failureRedirect: '/login',
session: false,
}),
function (req, res) {
socialRedirect(res, req.query.state, req.user.token, config);
}
},
);
router.use('/', require('../helpers').commonErrorHandler);
function socialRedirect(res, state, token, config) {
res.redirect(config.uiUrl + "/login?token=" + token);
res.redirect(config.uiUrl + '/login?token=' + token);
}
module.exports = router;

View File

@ -2,6 +2,11 @@ const Element_type_defaultsService = require('../services/element_type_defaults'
const Element_type_defaultsDBApi = require('../db/api/element_type_defaults');
const { createEntityRouter } = require('../factories/router.factory');
module.exports = createEntityRouter('element_type_defaults', Element_type_defaultsService, Element_type_defaultsDBApi, {
permissionEntity: 'page_elements',
});
module.exports = createEntityRouter(
'element_type_defaults',
Element_type_defaultsService,
Element_type_defaultsDBApi,
{
permissionEntity: 'page_elements',
},
);

View File

@ -31,9 +31,13 @@ router.post('/presign', jsonParser, async (req, res) => {
}
// Validate that all URLs are strings
const invalidUrls = urls.filter((url) => typeof url !== 'string' || !url.trim());
const invalidUrls = urls.filter(
(url) => typeof url !== 'string' || !url.trim(),
);
if (invalidUrls.length > 0) {
return res.status(400).json({ error: 'All URLs must be non-empty strings' });
return res
.status(400)
.json({ error: 'All URLs must be non-empty strings' });
}
try {
@ -45,11 +49,15 @@ router.post('/presign', jsonParser, async (req, res) => {
}
});
router.post('/upload/:table/:field', passport.authenticate('jwt', {session: false}), (req, res) => {
const fileName = `${req.params.table}/${req.params.field}`;
router.post(
'/upload/:table/:field',
passport.authenticate('jwt', { session: false }),
(req, res) => {
const fileName = `${req.params.table}/${req.params.field}`;
services.uploadFile(fileName, req, res);
});
services.uploadFile(fileName, req, res);
},
);
router.post(
'/upload-sessions/init',

View File

@ -7,17 +7,22 @@ const { getWidget, askGpt } = require('../services/openai');
const { LocalAIApi } = require('../ai/LocalAIApi');
const loadRolesModules = () => {
try {
return {
RolesService: require('../services/roles'),
RolesDBApi: require('../db/api/roles'),
};
} catch (error) {
console.error('Roles modules are missing. Advanced roles are required for this endpoint.', error);
const err = new Error('Roles modules are missing. Advanced roles are required for this endpoint.');
err.originalError = error;
throw err;
}
try {
return {
RolesService: require('../services/roles'),
RolesDBApi: require('../db/api/roles'),
};
} catch (error) {
console.error(
'Roles modules are missing. Advanced roles are required for this endpoint.',
error,
);
const err = new Error(
'Roles modules are missing. Advanced roles are required for this endpoint.',
);
err.originalError = error;
throw err;
}
};
/**
@ -70,18 +75,18 @@ const loadRolesModules = () => {
*/
router.delete(
'/roles-info/:infoId',
wrapAsync(async (req, res) => {
const { RolesService } = loadRolesModules();
const role = await RolesService.removeRoleInfoById(
req.query.infoId,
req.query.roleId,
req.query.key,
req.currentUser,
);
'/roles-info/:infoId',
wrapAsync(async (req, res) => {
const { RolesService } = loadRolesModules();
const role = await RolesService.removeRoleInfoById(
req.query.infoId,
req.query.roleId,
req.query.key,
req.currentUser,
);
res.status(200).send(role);
}),
res.status(200).send(role);
}),
);
/**
@ -128,83 +133,80 @@ router.delete(
*/
router.get(
'/info-by-key',
wrapAsync(async (req, res) => {
const { RolesService, RolesDBApi } = loadRolesModules();
const roleId = req.query.roleId;
const key = req.query.key;
const currentUser = req.currentUser;
let info = await RolesService.getRoleInfoByKey(
key,
roleId,
currentUser,
);
const role = await RolesDBApi.findBy({ id: roleId });
if (!role?.role_customization) {
try {
await Promise.all(["pie", "bar"].map(async (e) => {
const schema = await sjs.getSequelizeSchema(db.sequelize, {});
const payload = {
description: `Create some cool ${e} chart`,
modelDefinition: schema.definitions,
};
const widgetId = await getWidget(payload, currentUser?.id, roleId);
if (widgetId) {
await RolesService.addRoleInfo(
roleId,
currentUser?.id,
'widgets',
widgetId,
req.currentUser,
);
}
}));
info = await RolesService.getRoleInfoByKey(
key,
roleId,
currentUser,
);
} catch (error) {
console.warn('Widget creation skipped (external API unavailable):', error.message);
// Return empty widgets when external API is unavailable
if (key === 'widgets') {
info = [];
}
'/info-by-key',
wrapAsync(async (req, res) => {
const { RolesService, RolesDBApi } = loadRolesModules();
const roleId = req.query.roleId;
const key = req.query.key;
const currentUser = req.currentUser;
let info = await RolesService.getRoleInfoByKey(key, roleId, currentUser);
const role = await RolesDBApi.findBy({ id: roleId });
if (!role?.role_customization) {
try {
await Promise.all(
['pie', 'bar'].map(async (e) => {
const schema = await sjs.getSequelizeSchema(db.sequelize, {});
const payload = {
description: `Create some cool ${e} chart`,
modelDefinition: schema.definitions,
};
const widgetId = await getWidget(payload, currentUser?.id, roleId);
if (widgetId) {
await RolesService.addRoleInfo(
roleId,
currentUser?.id,
'widgets',
widgetId,
req.currentUser,
);
}
}),
);
info = await RolesService.getRoleInfoByKey(key, roleId, currentUser);
} catch (error) {
console.warn(
'Widget creation skipped (external API unavailable):',
error.message,
);
// Return empty widgets when external API is unavailable
if (key === 'widgets') {
info = [];
}
res.status(200).send(info);
}),
}
}
res.status(200).send(info);
}),
);
router.post(
'/create_widget',
wrapAsync(async (req, res) => {
const { RolesService } = loadRolesModules();
const { description, userId, roleId } = req.body;
'/create_widget',
wrapAsync(async (req, res) => {
const { RolesService } = loadRolesModules();
const { description, userId, roleId } = req.body;
const currentUser = req.currentUser;
const schema = await sjs.getSequelizeSchema(db.sequelize, {});
const payload = {
description,
modelDefinition: schema.definitions,
};
const currentUser = req.currentUser;
const schema = await sjs.getSequelizeSchema(db.sequelize, {});
const payload = {
description,
modelDefinition: schema.definitions,
};
const widgetId = await getWidget(payload, userId, roleId);
const widgetId = await getWidget(payload, userId, roleId);
if (widgetId) {
await RolesService.addRoleInfo(
roleId,
userId,
'widgets',
widgetId,
currentUser,
);
if (widgetId) {
await RolesService.addRoleInfo(
roleId,
userId,
'widgets',
widgetId,
currentUser,
);
return res.status(200).send(widgetId);
} else {
return res.status(400).send(widgetId);
}
}),
return res.status(200).send(widgetId);
} else {
return res.status(400).send(widgetId);
}
}),
);
/**
@ -252,23 +254,23 @@ router.post(
* description: Proxy error
*/
router.post(
'/response',
wrapAsync(async (req, res) => {
const body = req.body || {};
const options = body.options || {};
const payload = { ...body };
delete payload.options;
'/response',
wrapAsync(async (req, res) => {
const body = req.body || {};
const options = body.options || {};
const payload = { ...body };
delete payload.options;
const response = await LocalAIApi.createResponse(payload, options);
const response = await LocalAIApi.createResponse(payload, options);
if (response.success) {
return res.status(200).send(response);
}
if (response.success) {
return res.status(200).send(response);
}
console.error('AI proxy error:', response);
const status = response.error === 'input_missing' ? 400 : 502;
return res.status(status).send(response);
}),
console.error('AI proxy error:', response);
const status = response.error === 'input_missing' ? 400 : 502;
return res.status(status).send(response);
}),
);
/**
@ -312,25 +314,24 @@ router.post(
* description: Some server error
*/
router.post(
'/ask-gpt',
wrapAsync(async (req, res) => {
const { prompt } = req.body;
if (!prompt) {
return res.status(400).send({
success: false,
error: 'Prompt is required',
});
}
'/ask-gpt',
wrapAsync(async (req, res) => {
const { prompt } = req.body;
if (!prompt) {
return res.status(400).send({
success: false,
error: 'Prompt is required',
});
}
const response = await askGpt(prompt);
const response = await askGpt(prompt);
if (response.success) {
return res.status(200).send(response);
} else {
return res.status(500).send(response);
}
}),
if (response.success) {
return res.status(200).send(response);
} else {
return res.status(500).send(response);
}
}),
);
module.exports = router;

View File

@ -1,2 +0,0 @@

View File

@ -181,4 +181,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('permissions', PermissionsService, PermissionsDBApi);
module.exports = createEntityRouter(
'permissions',
PermissionsService,
PermissionsDBApi,
);

View File

@ -143,4 +143,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('presigned_url_requests', Presigned_url_requestsService, Presigned_url_requestsDBApi);
module.exports = createEntityRouter(
'presigned_url_requests',
Presigned_url_requestsService,
Presigned_url_requestsDBApi,
);

View File

@ -144,4 +144,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('project_audio_tracks', Project_audio_tracksService, Project_audio_tracksDBApi);
module.exports = createEntityRouter(
'project_audio_tracks',
Project_audio_tracksService,
Project_audio_tracksDBApi,
);

View File

@ -10,7 +10,7 @@ const baseRouter = createEntityRouter(
Project_element_defaultsDBApi,
{
permissionEntity: 'page_elements',
}
},
);
/**
@ -40,10 +40,10 @@ baseRouter.post(
wrapAsync(async (req, res) => {
const payload = await Project_element_defaultsService.resetToGlobal(
req.params.id,
{ currentUser: req.currentUser }
{ currentUser: req.currentUser },
);
res.status(200).json(payload);
})
}),
);
/**
@ -72,10 +72,10 @@ baseRouter.get(
'/:id/diff',
wrapAsync(async (req, res) => {
const payload = await Project_element_defaultsService.getDiffFromGlobal(
req.params.id
req.params.id,
);
res.status(200).json(payload);
})
}),
);
module.exports = baseRouter;

View File

@ -138,4 +138,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('project_memberships', Project_membershipsService, Project_membershipsDBApi);
module.exports = createEntityRouter(
'project_memberships',
Project_membershipsService,
Project_membershipsDBApi,
);

View File

@ -1,23 +1,17 @@
const express = require('express');
const ProjectsService = require('../services/projects');
const ProjectsDBApi = require('../db/api/projects');
const { wrapAsync, isUuidV4 } = require('../helpers');
const router = express.Router();
const { parse } = require('json2csv');
const {
checkCrudPermissions,
} = require('../middlewares/check-permissions');
const { checkCrudPermissions } = require('../middlewares/check-permissions');
router.use(checkCrudPermissions('projects'));
/**
* @swagger
* components:
@ -67,53 +61,69 @@ router.use(checkCrudPermissions('projects'));
*/
/**
* @swagger
* /api/projects:
* post:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Add new item
* description: Add new item
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Projects"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*/
router.post('/', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
* @swagger
* /api/projects:
* post:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Add new item
* description: Add new item
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Projects"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*/
router.post(
'/',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
const payload = await ProjectsService.create(req.body.data, req.currentUser, true, link.host);
const payload = await ProjectsService.create(
req.body.data,
req.currentUser,
true,
link.host,
);
res.status(200).send(payload);
}));
}),
);
router.post('/:id/clone', wrapAsync(async (req, res) => {
router.post(
'/:id/clone',
wrapAsync(async (req, res) => {
if (!isUuidV4(req.params.id)) {
return res.status(400).send('Invalid project id');
}
const payload = await ProjectsService.cloneFromProject(req.params.id, req.currentUser);
const payload = await ProjectsService.cloneFromProject(
req.params.id,
req.currentUser,
);
res.status(200).send(payload);
}));
}),
);
/**
* @swagger
@ -150,193 +160,220 @@ router.post('/:id/clone', wrapAsync(async (req, res) => {
* description: Some server error
*
*/
router.post('/bulk-import', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
router.post(
'/bulk-import',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await ProjectsService.bulkImport(req, res, true, link.host);
const payload = true;
res.status(200).send(payload);
}));
}),
);
/**
* @swagger
* /api/projects/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Update the data of the selected item
* description: Update the data of the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to update
* required: true
* schema:
* type: string
* requestBody:
* description: Set new item data
* required: true
* content:
* application/json:
* schema:
* properties:
* id:
* description: ID of the updated item
* type: string
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Projects"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.put('/:id', wrapAsync(async (req, res) => {
await ProjectsService.update(req.body.data, req.body.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}));
* @swagger
* /api/projects/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Update the data of the selected item
* description: Update the data of the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to update
* required: true
* schema:
* type: string
* requestBody:
* description: Set new item data
* required: true
* content:
* application/json:
* schema:
* properties:
* id:
* description: ID of the updated item
* type: string
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Projects"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.put(
'/:id',
wrapAsync(async (req, res) => {
await ProjectsService.update(req.body.data, req.body.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/projects/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Delete the selected item
* description: Delete the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to delete
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The item was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.delete('/:id', wrapAsync(async (req, res) => {
await ProjectsService.remove(req.params.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}));
* @swagger
* /api/projects/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Delete the selected item
* description: Delete the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to delete
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The item was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.delete(
'/:id',
wrapAsync(async (req, res) => {
await ProjectsService.remove(req.params.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/projects/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Delete the selected item list
* description: Delete the selected item list
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* ids:
* description: IDs of the updated items
* type: array
* responses:
* 200:
* description: The items was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post('/deleteByIds', wrapAsync(async (req, res) => {
* @swagger
* /api/projects/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Delete the selected item list
* description: Delete the selected item list
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* ids:
* description: IDs of the updated items
* type: array
* responses:
* 200:
* description: The items was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post(
'/deleteByIds',
wrapAsync(async (req, res) => {
await ProjectsService.deleteByIds(req.body.data, req.currentUser);
const payload = true;
res.status(200).send(payload);
}));
}),
);
/**
* @swagger
* /api/projects:
* get:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Get all projects
* description: Get all projects
* responses:
* 200:
* description: Projects list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Projects"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/', wrapAsync(async (req, res) => {
const filetype = req.query.filetype
const currentUser = req.currentUser;
const runtimeContext = req.runtimeContext;
const payload = await ProjectsDBApi.findAll(
req.query, { currentUser, runtimeContext }
);
if (filetype && filetype === 'csv') {
const fields = ['id','name','slug','description','logo_url','favicon_url','og_image_url','theme_config_json','custom_css_json','cdn_base_url'];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv)
* @swagger
* /api/projects:
* get:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Get all projects
* description: Get all projects
* responses:
* 200:
* description: Projects list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Projects"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get(
'/',
wrapAsync(async (req, res) => {
const filetype = req.query.filetype;
} catch (err) {
console.error(err);
const currentUser = req.currentUser;
const runtimeContext = req.runtimeContext;
const payload = await ProjectsDBApi.findAll(req.query, {
currentUser,
runtimeContext,
});
if (filetype && filetype === 'csv') {
const fields = [
'id',
'name',
'slug',
'description',
'logo_url',
'favicon_url',
'og_image_url',
'theme_config_json',
'custom_css_json',
'cdn_base_url',
];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv);
} catch (err) {
console.error(err);
}
} else {
res.status(200).send(payload);
}
} else {
res.status(200).send(payload);
}
}));
}),
);
/**
* @swagger
@ -363,17 +400,20 @@ router.get('/', wrapAsync(async (req, res) => {
* 500:
* description: Some server error
*/
router.get('/count', wrapAsync(async (req, res) => {
router.get(
'/count',
wrapAsync(async (req, res) => {
const currentUser = req.currentUser;
const runtimeContext = req.runtimeContext;
const payload = await ProjectsDBApi.findAll(
req.query,
{ countOnly: true, currentUser, runtimeContext }
);
const payload = await ProjectsDBApi.findAll(req.query, {
countOnly: true,
currentUser,
runtimeContext,
});
res.status(200).send(payload);
}));
}),
);
/**
* @swagger
@ -401,64 +441,63 @@ router.get('/count', wrapAsync(async (req, res) => {
* description: Some server error
*/
router.get('/autocomplete', async (req, res) => {
const payload = await ProjectsDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
);
res.status(200).send(payload);
});
/**
* @swagger
* /api/projects/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get('/:id', wrapAsync(async (req, res) => {
if (!isUuidV4(req.params.id)) {
return res.status(400).send('Invalid project id');
}
* @swagger
* /api/projects/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Projects]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Projects"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get(
'/:id',
wrapAsync(async (req, res) => {
if (!isUuidV4(req.params.id)) {
return res.status(400).send('Invalid project id');
}
const runtimeContext = req.runtimeContext;
const payload = await ProjectsDBApi.findBy(
{ id: req.params.id },
{ runtimeContext },
);
const runtimeContext = req.runtimeContext;
const payload = await ProjectsDBApi.findBy(
{ id: req.params.id },
{ runtimeContext },
);
res.status(200).send(payload);
}));
res.status(200).send(payload);
}),
);
/**
* @swagger
@ -493,21 +532,24 @@ router.get('/:id', wrapAsync(async (req, res) => {
* 500:
* description: Server error
*/
router.get('/:id/offline-manifest', wrapAsync(async (req, res) => {
if (!isUuidV4(req.params.id)) {
return res.status(400).send('Invalid project id');
}
router.get(
'/:id/offline-manifest',
wrapAsync(async (req, res) => {
if (!isUuidV4(req.params.id)) {
return res.status(400).send('Invalid project id');
}
const PWAManifestService = require('../services/pwa_manifest');
const { variant = 'desktop' } = req.query;
const PWAManifestService = require('../services/pwa_manifest');
const { variant = 'desktop' } = req.query;
const manifest = await PWAManifestService.generateManifest(
req.params.id,
variant
);
const manifest = await PWAManifestService.generateManifest(
req.params.id,
variant,
);
res.status(200).json(manifest);
}));
res.status(200).json(manifest);
}),
);
router.use('/', require('../helpers').commonErrorHandler);

View File

@ -9,7 +9,12 @@ router.use(checkCrudPermissions('publish_events'));
const publishHandler = wrapAsync(async (req, res) => {
const { projectId, title, description } = req.body;
const result = await PublishService.publishToProduction(projectId, req.currentUser, title, description);
const result = await PublishService.publishToProduction(
projectId,
req.currentUser,
title,
description,
);
res.status(200).send(result);
});

View File

@ -150,4 +150,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('publish_events', Publish_eventsService, Publish_eventsDBApi);
module.exports = createEntityRouter(
'publish_events',
Publish_eventsService,
Publish_eventsDBApi,
);

View File

@ -141,4 +141,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('pwa_caches', Pwa_cachesService, Pwa_cachesDBApi);
module.exports = createEntityRouter(
'pwa_caches',
Pwa_cachesService,
Pwa_cachesDBApi,
);

View File

@ -3,7 +3,9 @@ const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.status(200).send(req.runtimeContext || { mode: 'unknown', projectSlug: null });
res
.status(200)
.send(req.runtimeContext || { mode: 'unknown', projectSlug: null });
});
module.exports = router;

View File

@ -1,7 +1,6 @@
const express = require('express');
const SearchService = require('../services/search');
const router = express.Router();
const { checkCrudPermissions } = require('../middlewares/check-permissions');
@ -34,19 +33,22 @@ router.use(checkCrudPermissions('search'));
*/
router.post('/', async (req, res) => {
const { searchQuery } = req.body;
if (!searchQuery) {
return res.status(400).json({ error: 'Please enter a search query' });
}
try {
const foundMatches = await SearchService.search(searchQuery, req.currentUser );
res.json(foundMatches);
} catch (error) {
console.error('Internal Server Error', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
const { searchQuery } = req.body;
module.exports = router;
if (!searchQuery) {
return res.status(400).json({ error: 'Please enter a search query' });
}
try {
const foundMatches = await SearchService.search(
searchQuery,
req.currentUser,
);
res.json(foundMatches);
} catch (error) {
console.error('Internal Server Error', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
module.exports = router;

View File

@ -71,13 +71,16 @@ router.post(
wrapAsync(async (req, res) => {
const { currentUser } = req;
const isAdminUser = Boolean(
currentUser
&& currentUser.app_role
&& (currentUser.app_role.name === 'Administrator' || currentUser.app_role.globalAccess === true),
currentUser &&
currentUser.app_role &&
(currentUser.app_role.name === 'Administrator' ||
currentUser.app_role.globalAccess === true),
);
if (!isAdminUser) {
return res.status(403).json({ error: 'Only administrators can execute SQL queries' });
return res
.status(403)
.json({ error: 'Only administrators can execute SQL queries' });
}
const { sql } = req.body;
@ -90,7 +93,10 @@ router.post(
const wrappedSql = `SELECT * FROM (${normalized}) AS query_result LIMIT ${MAX_SQL_ROWS}`;
const rows = await db.sequelize.transaction(async (transaction) => {
await db.sequelize.query(`SET LOCAL statement_timeout = ${SQL_TIMEOUT_MS}`, { transaction });
await db.sequelize.query(
`SET LOCAL statement_timeout = ${SQL_TIMEOUT_MS}`,
{ transaction },
);
return db.sequelize.query(wrappedSql, {
transaction,
type: db.Sequelize.QueryTypes.SELECT,

View File

@ -148,4 +148,8 @@ const { createEntityRouter } = require('../factories/router.factory');
* description: Some server error
*/
module.exports = createEntityRouter('tour_pages', Tour_pagesService, Tour_pagesDBApi);
module.exports = createEntityRouter(
'tour_pages',
Tour_pagesService,
Tour_pagesDBApi,
);

View File

@ -1,23 +1,17 @@
const express = require('express');
const UsersService = require('../services/users');
const UsersDBApi = require('../db/api/users');
const wrapAsync = require('../helpers').wrapAsync;
const router = express.Router();
const { parse } = require('json2csv');
const {
checkCrudPermissions,
} = require('../middlewares/check-permissions');
const { checkCrudPermissions } = require('../middlewares/check-permissions');
router.use(checkCrudPermissions('users'));
/**
* @swagger
* components:
@ -51,45 +45,50 @@ router.use(checkCrudPermissions('users'));
*/
/**
* @swagger
* /api/users:
* post:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Add new item
* description: Add new item
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Users"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*/
router.post('/', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
* @swagger
* /api/users:
* post:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Add new item
* description: Add new item
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Users"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 405:
* description: Invalid input data
* 500:
* description: Some server error
*/
router.post(
'/',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await UsersService.create(req.body.data, req.currentUser, true, link.host);
const payload = true;
res.status(200).send(payload);
}));
}),
);
/**
* @swagger
@ -126,196 +125,205 @@ router.post('/', wrapAsync(async (req, res) => {
* description: Some server error
*
*/
router.post('/bulk-import', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
router.post(
'/bulk-import',
wrapAsync(async (req, res) => {
const referer =
req.headers.referer ||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
await UsersService.bulkImport(req, res, true, link.host);
const payload = true;
res.status(200).send(payload);
}));
}),
);
/**
* @swagger
* /api/users/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Update the data of the selected item
* description: Update the data of the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to update
* required: true
* schema:
* type: string
* requestBody:
* description: Set new item data
* required: true
* content:
* application/json:
* schema:
* properties:
* id:
* description: ID of the updated item
* type: string
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Users"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.put('/:id', wrapAsync(async (req, res) => {
await UsersService.update(req.body.data, req.body.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}));
* @swagger
* /api/users/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Update the data of the selected item
* description: Update the data of the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to update
* required: true
* schema:
* type: string
* requestBody:
* description: Set new item data
* required: true
* content:
* application/json:
* schema:
* properties:
* id:
* description: ID of the updated item
* type: string
* data:
* description: Data of the updated item
* type: object
* $ref: "#/components/schemas/Users"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.put(
'/:id',
wrapAsync(async (req, res) => {
await UsersService.update(req.body.data, req.body.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/users/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Delete the selected item
* description: Delete the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to delete
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The item was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.delete('/:id', wrapAsync(async (req, res) => {
await UsersService.remove(req.params.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}));
* @swagger
* /api/users/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Delete the selected item
* description: Delete the selected item
* parameters:
* - in: path
* name: id
* description: Item ID to delete
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The item was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.delete(
'/:id',
wrapAsync(async (req, res) => {
await UsersService.remove(req.params.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/users/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Delete the selected item list
* description: Delete the selected item list
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* ids:
* description: IDs of the updated items
* type: array
* responses:
* 200:
* description: The items was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post('/deleteByIds', wrapAsync(async (req, res) => {
* @swagger
* /api/users/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Delete the selected item list
* description: Delete the selected item list
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* ids:
* description: IDs of the updated items
* type: array
* responses:
* 200:
* description: The items was successfully deleted
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post(
'/deleteByIds',
wrapAsync(async (req, res) => {
await UsersService.deleteByIds(req.body.data, req.currentUser);
const payload = true;
res.status(200).send(payload);
}));
}),
);
/**
* @swagger
* /api/users:
* get:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Get all users
* description: Get all users
* responses:
* 200:
* description: Users list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Users"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/', wrapAsync(async (req, res) => {
const filetype = req.query.filetype
const currentUser = req.currentUser;
const payload = await UsersDBApi.findAll(
req.query, { currentUser }
);
if (filetype && filetype === 'csv') {
const fields = ['id','firstName','lastName','phoneNumber','email',
];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv)
* @swagger
* /api/users:
* get:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Get all users
* description: Get all users
* responses:
* 200:
* description: Users list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Users"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get(
'/',
wrapAsync(async (req, res) => {
const filetype = req.query.filetype;
} catch (err) {
console.error(err);
const currentUser = req.currentUser;
const payload = await UsersDBApi.findAll(req.query, { currentUser });
if (filetype && filetype === 'csv') {
const fields = ['id', 'firstName', 'lastName', 'phoneNumber', 'email'];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv);
} catch (err) {
console.error(err);
}
} else {
res.status(200).send(payload);
}
} else {
res.status(200).send(payload);
}
}));
}),
);
/**
* @swagger
@ -342,17 +350,18 @@ router.get('/', wrapAsync(async (req, res) => {
* 500:
* description: Some server error
*/
router.get('/count', wrapAsync(async (req, res) => {
router.get(
'/count',
wrapAsync(async (req, res) => {
const currentUser = req.currentUser;
const payload = await UsersDBApi.findAll(
req.query,
null,
{ countOnly: true, currentUser }
);
const payload = await UsersDBApi.findAll(req.query, null, {
countOnly: true,
currentUser,
});
res.status(200).send(payload);
}));
}),
);
/**
* @swagger
@ -380,60 +389,57 @@ router.get('/count', wrapAsync(async (req, res) => {
* description: Some server error
*/
router.get('/autocomplete', async (req, res) => {
const payload = await UsersDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
);
res.status(200).send(payload);
});
/**
* @swagger
* /api/users/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await UsersDBApi.findBy(
{ id: req.params.id },
);
delete payload.password;
* @swagger
* /api/users/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Users]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Users"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get(
'/:id',
wrapAsync(async (req, res) => {
const payload = await UsersDBApi.findBy({ id: req.params.id });
res.status(200).send(payload);
}));
delete payload.password;
res.status(200).send(payload);
}),
);
router.use('/', require('../helpers').commonErrorHandler);

View File

@ -4,7 +4,7 @@ const ValidationError = require('./notifications/errors/validation');
const ForbiddenError = require('./notifications/errors/forbidden');
const bcrypt = require('bcrypt');
const EmailAddressVerificationEmail = require('./email/list/addressVerification');
const InvitationEmail = require("./email/list/invitation");
const InvitationEmail = require('./email/list/invitation');
const PasswordResetEmail = require('./email/list/passwordReset');
const EmailSender = require('./email');
const config = require('../config');
@ -12,7 +12,7 @@ const helpers = require('../helpers');
class Auth {
static async signup(email, password, options = {}, host) {
const user = await UsersDBApi.findBy({email});
const user = await UsersDBApi.findBy({ email });
const hashedPassword = await bcrypt.hash(
password,
@ -21,35 +21,24 @@ class Auth {
if (user) {
if (user.authenticationUid) {
throw new ValidationError(
'auth.emailAlreadyInUse',
);
throw new ValidationError('auth.emailAlreadyInUse');
}
if (user.disabled) {
throw new ValidationError(
'auth.userDisabled',
);
throw new ValidationError('auth.userDisabled');
}
await UsersDBApi.updatePassword(
user.id,
hashedPassword,
options,
);
await UsersDBApi.updatePassword(user.id, hashedPassword, options);
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(
user.email,
host,
);
await this.sendEmailAddressVerificationEmail(user.email, host);
}
const data = {
user: {
id: user.id,
email: user.email
}
email: user.email,
},
};
return helpers.jwtSign(data);
@ -60,47 +49,37 @@ class Auth {
firstName: email.split('@')[0],
password: hashedPassword,
email: email,
},
options,
);
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(
newUser.email,
host,
);
await this.sendEmailAddressVerificationEmail(newUser.email, host);
}
const data = {
user: {
id: newUser.id,
email: newUser.email
}
email: newUser.email,
},
};
return helpers.jwtSign(data);
}
static async signin(email, password) {
const user = await UsersDBApi.findBy({email});
const user = await UsersDBApi.findBy({ email });
if (!user) {
throw new ValidationError(
'auth.userNotFound',
);
throw new ValidationError('auth.userNotFound');
}
if (user.disabled) {
throw new ValidationError(
'auth.userDisabled',
);
throw new ValidationError('auth.userDisabled');
}
if (!user.password) {
throw new ValidationError(
'auth.wrongPassword',
);
throw new ValidationError('auth.wrongPassword');
}
if (!EmailSender.isConfigured) {
@ -108,49 +87,33 @@ class Auth {
}
if (!user.emailVerified) {
throw new ValidationError(
'auth.userNotVerified',
);
throw new ValidationError('auth.userNotVerified');
}
const passwordsMatch = await bcrypt.compare(
password,
user.password,
);
const passwordsMatch = await bcrypt.compare(password, user.password);
if (!passwordsMatch) {
throw new ValidationError(
'auth.wrongPassword',
);
throw new ValidationError('auth.wrongPassword');
}
const data = {
user: {
id: user.id,
email: user.email
}
email: user.email,
},
};
return helpers.jwtSign(data);
}
static async sendEmailAddressVerificationEmail(
email,
host,
) {
static async sendEmailAddressVerificationEmail(email, host) {
let link;
try {
const token = await UsersDBApi.generateEmailVerificationToken(
email,
);
const token = await UsersDBApi.generateEmailVerificationToken(email);
link = `${host}/verify-email?token=${token}`;
} catch (error) {
console.error(error);
throw new ValidationError(
'auth.emailAddressVerificationEmail.error',
);
throw new ValidationError('auth.emailAddressVerificationEmail.error');
}
const emailAddressVerificationEmail = new EmailAddressVerificationEmail(
@ -158,50 +121,33 @@ class Auth {
link,
);
return new EmailSender(
emailAddressVerificationEmail,
).send();
return new EmailSender(emailAddressVerificationEmail).send();
}
static async sendPasswordResetEmail(email, type = 'register', host) {
let link;
try {
const token = await UsersDBApi.generatePasswordResetToken(
email,
);
const token = await UsersDBApi.generatePasswordResetToken(email);
link = `${host}/password-reset?token=${token}`;
} catch (error) {
console.error(error);
throw new ValidationError(
'auth.passwordReset.error',
);
throw new ValidationError('auth.passwordReset.error');
}
let passwordResetEmail;
if (type === 'register') {
passwordResetEmail = new PasswordResetEmail(
email,
link,
);
passwordResetEmail = new PasswordResetEmail(email, link);
}
if (type === 'invitation') {
passwordResetEmail = new InvitationEmail(
email,
link,
);
passwordResetEmail = new InvitationEmail(email, link);
}
return new EmailSender(passwordResetEmail).send();
}
static async verifyEmail(token, options = {}) {
const user = await UsersDBApi.findByEmailVerificationToken(
token,
options,
);
const user = await UsersDBApi.findByEmailVerificationToken(token, options);
if (!user) {
throw new ValidationError(
@ -209,10 +155,7 @@ class Auth {
);
}
return UsersDBApi.markEmailVerified(
user.id,
options,
);
return UsersDBApi.markEmailVerified(user.id, options);
}
static async passwordUpdate(currentPassword, newPassword, options) {
@ -227,9 +170,7 @@ class Auth {
);
if (!currentPasswordMatch) {
throw new ValidationError(
'auth.wrongPassword'
)
throw new ValidationError('auth.wrongPassword');
}
const newPasswordMatch = await bcrypt.compare(
@ -238,9 +179,7 @@ class Auth {
);
if (newPasswordMatch) {
throw new ValidationError(
'auth.passwordUpdate.samePassword'
)
throw new ValidationError('auth.passwordUpdate.samePassword');
}
const hashedPassword = await bcrypt.hash(
@ -248,27 +187,14 @@ class Auth {
config.bcrypt.saltRounds,
);
return UsersDBApi.updatePassword(
currentUser.id,
hashedPassword,
options,
);
return UsersDBApi.updatePassword(currentUser.id, hashedPassword, options);
}
static async passwordReset(
token,
password,
options = {},
) {
const user = await UsersDBApi.findByPasswordResetToken(
token,
options,
);
static async passwordReset(token, password, options = {}) {
const user = await UsersDBApi.findByPasswordResetToken(token, options);
if (!user) {
throw new ValidationError(
'auth.passwordReset.invalidToken',
);
throw new ValidationError('auth.passwordReset.invalidToken');
}
const hashedPassword = await bcrypt.hash(
@ -276,31 +202,19 @@ class Auth {
config.bcrypt.saltRounds,
);
return UsersDBApi.updatePassword(
user.id,
hashedPassword,
options,
);
return UsersDBApi.updatePassword(user.id, hashedPassword, options);
}
static async updateProfile(data, currentUser) {
let transaction = await db.sequelize.transaction();
try {
await UsersDBApi.findBy(
{id: currentUser.id},
{transaction},
);
await UsersDBApi.update(
currentUser.id,
data,
{
currentUser,
transaction
},
);
await UsersDBApi.findBy({ id: currentUser.id }, { transaction });
await UsersDBApi.update(currentUser.id, data, {
currentUser,
transaction,
});
await transaction.commit();
} catch (error) {

View File

@ -10,23 +10,27 @@ module.exports = class EmailAddressVerificationEmail {
get subject() {
return getNotification(
'emails.emailAddressVerification.subject',
getNotification('app.title'),
'emails.emailAddressVerification.subject',
getNotification('app.title'),
);
}
async html() {
try {
const templatePath = path.join(__dirname, '../../email/htmlTemplates/addressVerification/emailAddressVerification.html');
const templatePath = path.join(
__dirname,
'../../email/htmlTemplates/addressVerification/emailAddressVerification.html',
);
const template = await fs.readFile(templatePath, 'utf8');
const appTitle = getNotification('app.title');
const signupUrl = this.link;
let html = template.replace(/{appTitle}/g, appTitle)
.replace(/{signupUrl}/g, signupUrl)
.replace(/{to}/g, this.to);
let html = template
.replace(/{appTitle}/g, appTitle)
.replace(/{signupUrl}/g, signupUrl)
.replace(/{to}/g, this.to);
return html;
} catch (error) {
@ -34,5 +38,4 @@ module.exports = class EmailAddressVerificationEmail {
throw error;
}
}
};

View File

@ -10,23 +10,27 @@ module.exports = class InvitationEmail {
get subject() {
return getNotification(
'emails.invitation.subject',
getNotification('app.title'),
'emails.invitation.subject',
getNotification('app.title'),
);
}
async html() {
try {
const templatePath = path.join(__dirname, '../../email/htmlTemplates/invitation/invitationTemplate.html');
const templatePath = path.join(
__dirname,
'../../email/htmlTemplates/invitation/invitationTemplate.html',
);
const template = await fs.readFile(templatePath, 'utf8');
const appTitle = getNotification('app.title');
const signupUrl = `${this.host}&invitation=true`;
let html = template.replace(/{appTitle}/g, appTitle)
.replace(/{signupUrl}/g, signupUrl)
.replace(/{to}/g, this.to);
let html = template
.replace(/{appTitle}/g, appTitle)
.replace(/{signupUrl}/g, signupUrl)
.replace(/{to}/g, this.to);
return html;
} catch (error) {
@ -34,4 +38,4 @@ module.exports = class InvitationEmail {
throw error;
}
}
};
};

View File

@ -1,6 +1,6 @@
const { getNotification } = require('../../notifications/helpers');
const path = require("path");
const {promises: fs} = require("fs");
const path = require('path');
const { promises: fs } = require('fs');
module.exports = class PasswordResetEmail {
constructor(to, link) {
@ -17,7 +17,10 @@ module.exports = class PasswordResetEmail {
async html() {
try {
const templatePath = path.join(__dirname, '../../email/htmlTemplates/passwordReset/passwordResetEmail.html');
const templatePath = path.join(
__dirname,
'../../email/htmlTemplates/passwordReset/passwordResetEmail.html',
);
const template = await fs.readFile(templatePath, 'utf8');
@ -25,9 +28,10 @@ module.exports = class PasswordResetEmail {
const resetUrl = this.link;
const accountName = this.to;
let html = template.replace(/{appTitle}/g, appTitle)
.replace(/{resetUrl}/g, resetUrl)
.replace(/{accountName}/g, accountName);
let html = template
.replace(/{appTitle}/g, appTitle)
.replace(/{resetUrl}/g, resetUrl)
.replace(/{accountName}/g, accountName);
return html;
} catch (error) {

View File

@ -4,7 +4,7 @@ const config = require('../config');
const path = require('path');
const { pipeline } = require('stream/promises');
const { v4: uuid } = require('uuid');
const { format } = require("util");
const { format } = require('util');
const {
S3Client,
PutObjectCommand,
@ -24,7 +24,7 @@ const ensureDirectoryExistence = (filePath) => {
ensureDirectoryExistence(dirname);
fs.mkdirSync(dirname);
}
};
const UPLOAD_SESSIONS_DIR = path.join(config.uploadDir, 'upload_sessions');
const UPLOAD_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
@ -39,7 +39,7 @@ const sanitizeFolder = (folder) => {
}
return value;
}
};
const sanitizeFilename = (filename) => {
const value = path.basename(String(filename || '').trim());
@ -49,11 +49,13 @@ const sanitizeFilename = (filename) => {
}
return value;
}
};
const getSessionDir = (sessionId) => path.join(UPLOAD_SESSIONS_DIR, sessionId);
const getSessionMetaPath = (sessionId) => path.join(getSessionDir(sessionId), 'meta.json');
const getSessionChunksDir = (sessionId) => path.join(getSessionDir(sessionId), 'chunks');
const getSessionMetaPath = (sessionId) =>
path.join(getSessionDir(sessionId), 'meta.json');
const getSessionChunksDir = (sessionId) =>
path.join(getSessionDir(sessionId), 'chunks');
const getSessionChunkPath = (sessionId, chunkIndex) =>
path.join(getSessionChunksDir(sessionId), `${String(chunkIndex)}.part`);
@ -66,13 +68,13 @@ const readSessionMeta = (sessionId) => {
const raw = fs.readFileSync(metaPath, 'utf8');
return JSON.parse(raw);
}
};
const writeSessionMeta = (sessionId, payload) => {
const metaPath = getSessionMetaPath(sessionId);
ensureDirectoryExistence(metaPath);
fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), 'utf8');
}
};
const removeUploadSession = (sessionId) => {
const sessionDir = getSessionDir(sessionId);
@ -80,7 +82,7 @@ const removeUploadSession = (sessionId) => {
if (fs.existsSync(sessionDir)) {
fs.rmSync(sessionDir, { recursive: true, force: true });
}
}
};
const cleanupExpiredUploadSessions = () => {
if (!fs.existsSync(UPLOAD_SESSIONS_DIR)) {
@ -98,7 +100,9 @@ const cleanupExpiredUploadSessions = () => {
return;
}
const updatedAt = new Date(meta.updatedAt || meta.createdAt || 0).getTime();
const updatedAt = new Date(
meta.updatedAt || meta.createdAt || 0,
).getTime();
if (!updatedAt || now - updatedAt > UPLOAD_SESSION_TTL_MS) {
removeUploadSession(sessionId);
}
@ -107,7 +111,7 @@ const cleanupExpiredUploadSessions = () => {
removeUploadSession(sessionId);
}
});
}
};
const streamAppendFile = async (targetPath, sourcePath) => {
await new Promise((resolve, reject) => {
@ -119,7 +123,7 @@ const streamAppendFile = async (targetPath, sourcePath) => {
writeStream.on('finish', resolve);
readStream.pipe(writeStream, { end: true });
});
}
};
// S3 session storage helpers
const S3_UPLOAD_SESSIONS_PREFIX = '_upload_sessions';
@ -163,7 +167,13 @@ const readS3SessionMeta = async (client, bucket, prefix, sessionId) => {
}
};
const writeS3SessionMeta = async (client, bucket, prefix, sessionId, payload) => {
const writeS3SessionMeta = async (
client,
bucket,
prefix,
sessionId,
payload,
) => {
const key = getS3SessionMetaKey(prefix, sessionId);
await client.send(
new PutObjectCommand({
@ -175,7 +185,14 @@ const writeS3SessionMeta = async (client, bucket, prefix, sessionId, payload) =>
);
};
const uploadS3Chunk = async (client, bucket, prefix, sessionId, chunkIndex, body) => {
const uploadS3Chunk = async (
client,
bucket,
prefix,
sessionId,
chunkIndex,
body,
) => {
const key = getS3SessionChunkKey(prefix, sessionId, chunkIndex);
await client.send(
new PutObjectCommand({
@ -186,7 +203,13 @@ const uploadS3Chunk = async (client, bucket, prefix, sessionId, chunkIndex, body
);
};
const downloadS3Chunk = async (client, bucket, prefix, sessionId, chunkIndex) => {
const downloadS3Chunk = async (
client,
bucket,
prefix,
sessionId,
chunkIndex,
) => {
const key = getS3SessionChunkKey(prefix, sessionId, chunkIndex);
try {
const output = await client.send(
@ -236,7 +259,9 @@ const removeS3UploadSession = async (client, bucket, prefix, sessionId) => {
return;
}
const objectsToDelete = listResult.Contents.map((obj) => ({ Key: obj.Key }));
const objectsToDelete = listResult.Contents.map((obj) => ({
Key: obj.Key,
}));
await client.send(
new DeleteObjectsCommand({
@ -291,12 +316,17 @@ const cleanupExpiredS3UploadSessions = async () => {
continue;
}
const updatedAt = new Date(meta.updatedAt || meta.createdAt || 0).getTime();
const updatedAt = new Date(
meta.updatedAt || meta.createdAt || 0,
).getTime();
if (!updatedAt || now - updatedAt > UPLOAD_SESSION_TTL_MS) {
await removeS3UploadSession(client, bucket, prefix, sessionId);
}
} catch (error) {
console.error(`Failed to cleanup S3 upload session ${sessionId}`, error);
console.error(
`Failed to cleanup S3 upload session ${sessionId}`,
error,
);
await removeS3UploadSession(client, bucket, prefix, sessionId);
}
}
@ -318,18 +348,13 @@ const uploadLocal = (
return;
}
if (
validations.entity
) {
if (validations.entity) {
res.sendStatus(403);
return;
}
if (validations.folderIncludesAuthenticationUid) {
folder = folder.replace(
':userId',
req.currentUser.authenticationUid,
);
folder = folder.replace(':userId', req.currentUser.authenticationUid);
if (
!req.currentUser.authenticationUid ||
!folder.includes(req.currentUser.authenticationUid)
@ -352,11 +377,7 @@ const uploadLocal = (
return;
}
const privateUrl = path.join(
form.uploadDir,
folder,
filename,
);
const privateUrl = path.join(form.uploadDir, folder, filename);
ensureDirectoryExistence(privateUrl);
fs.renameSync(fileTempUrl, privateUrl);
res.sendStatus(200);
@ -365,17 +386,17 @@ const uploadLocal = (
form.on('error', function (err) {
res.status(500).send(err);
});
}
}
};
};
const downloadLocal = async (req, res) => {
const privateUrl = req.query.privateUrl;
if (!privateUrl) {
return res.sendStatus(404);
}
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.download(path.join(config.uploadDir, privateUrl));
}
const privateUrl = req.query.privateUrl;
if (!privateUrl) {
return res.sendStatus(404);
}
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.download(path.join(config.uploadDir, privateUrl));
};
const deleteLocal = async (privateUrl) => {
try {
@ -392,30 +413,32 @@ const deleteLocal = async (privateUrl) => {
console.error(`Cannot delete local file ${privateUrl}`, error);
throw error;
}
}
};
const initGCloud = () => {
const processFile = require("../middlewares/upload");
const { Storage } = require("@google-cloud/storage");
const processFile = require('../middlewares/upload');
const { Storage } = require('@google-cloud/storage');
const hash = config.gcloud.hash
const hash = config.gcloud.hash;
const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, "\n");
const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, '\n');
const storage = new Storage({
projectId: process.env.GC_PROJECT_ID,
credentials: {
client_email: process.env.GC_CLIENT_EMAIL,
private_key: privateKey
}
projectId: process.env.GC_PROJECT_ID,
credentials: {
client_email: process.env.GC_CLIENT_EMAIL,
private_key: privateKey,
},
});
const bucket = storage.bucket(config.gcloud.bucket);
return {hash, bucket, processFile};
}
return { hash, bucket, processFile };
};
const getFileStorageProvider = () => {
const provider = (process.env.FILE_STORAGE_PROVIDER || '').trim().toLowerCase();
const provider = (process.env.FILE_STORAGE_PROVIDER || '')
.trim()
.toLowerCase();
if (provider) {
return provider;
@ -445,7 +468,7 @@ const getFileStorageProvider = () => {
}
return 'local';
}
};
const initS3 = () => {
const processFile = require('../middlewares/upload');
@ -464,7 +487,7 @@ const initS3 = () => {
prefix: config.s3.prefix,
processFile,
};
}
};
const buildStoragePath = (prefix, privateUrl) => {
const cleanPrefix = (prefix || '').replace(/^\/+|\/+$/g, '');
@ -475,17 +498,17 @@ const buildStoragePath = (prefix, privateUrl) => {
}
return `${cleanPrefix}/${cleanPrivateUrl}`;
}
};
const uploadGCloud = async (folder, req, res) => {
try {
const {hash, bucket, processFile} = initGCloud();
const { hash, bucket, processFile } = initGCloud();
await processFile(req, res);
let buffer = await req.file.buffer;
let filename = await req.body.filename;
if (!req.file) {
return res.status(400).send({ message: "Please upload a file!" });
return res.status(400).send({ message: 'Please upload a file!' });
}
let path = `${hash}/${folder}/${filename}`;
@ -497,7 +520,7 @@ const uploadGCloud = async (folder, req, res) => {
resumable: false,
});
blobStream.on("error", (err) => {
blobStream.on('error', (err) => {
console.log('Upload error');
console.log(err.message);
res.status(500).send({ message: err.message });
@ -505,52 +528,51 @@ const uploadGCloud = async (folder, req, res) => {
console.log(`https://storage.googleapis.com/${bucket.name}/${blob.name}`);
blobStream.on("finish", async () => {
blobStream.on('finish', async () => {
const publicUrl = format(
`https://storage.googleapis.com/${bucket.name}/${blob.name}`
`https://storage.googleapis.com/${bucket.name}/${blob.name}`,
);
res.status(200).send({
message: "Uploaded the file successfully: " + path,
message: 'Uploaded the file successfully: ' + path,
url: publicUrl,
});
});
blobStream.end(buffer)
blobStream.end(buffer);
} catch (err) {
console.log(err);
res.status(500).send({
message: `Could not upload the file. ${err}`
message: `Could not upload the file. ${err}`,
});
}
}
};
const downloadGCloud = async (req, res) => {
try {
const {hash, bucket} = initGCloud();
const { hash, bucket } = initGCloud();
const privateUrl = await req.query.privateUrl;
const filePath = `${hash}/${privateUrl}`;
const file = bucket.file(filePath)
const file = bucket.file(filePath);
const fileExists = await file.exists();
if (fileExists[0]) {
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
const stream = file.createReadStream();
stream.pipe(res);
}
else {
} else {
res.status(404).send({
message: "Could not download the file.",
});
message: 'Could not download the file.',
});
}
} catch (err) {
res.status(404).send({
message: "Could not download the file. " + err,
message: 'Could not download the file. ' + err,
});
}
}
};
const uploadS3 = async (folder, req, res) => {
try {
@ -558,13 +580,13 @@ const uploadS3 = async (folder, req, res) => {
await processFile(req, res);
if (!req.file) {
return res.status(400).send({ message: "Please upload a file!" });
return res.status(400).send({ message: 'Please upload a file!' });
}
const filename = req.body.filename;
if (!filename) {
return res.status(400).send({ message: "Missing filename" });
return res.status(400).send({ message: 'Missing filename' });
}
const privateUrl = `${folder}/${filename}`;
@ -589,14 +611,14 @@ const uploadS3 = async (folder, req, res) => {
message: `Could not upload the file. ${error.message || error}`,
});
}
}
};
const downloadS3 = async (req, res) => {
try {
const privateUrl = req.query.privateUrl;
if (!privateUrl) {
return res.status(404).send({ message: "Missing privateUrl" });
return res.status(404).send({ message: 'Missing privateUrl' });
}
const { client, bucket, prefix } = initS3();
@ -610,7 +632,7 @@ const downloadS3 = async (req, res) => {
if (!output || !output.Body) {
return res.status(404).send({
message: "Could not download the file.",
message: 'Could not download the file.',
});
}
@ -637,14 +659,14 @@ const downloadS3 = async (req, res) => {
message: `Could not download the file. ${error.message || error}`,
});
}
}
};
const deleteGCloud = async (privateUrl) => {
try {
const {hash, bucket} = initGCloud();
const { hash, bucket } = initGCloud();
const filePath = `${hash}/${privateUrl}`;
const file = bucket.file(filePath)
const file = bucket.file(filePath);
const fileExists = await file.exists();
if (fileExists[0]) {
@ -653,7 +675,7 @@ const deleteGCloud = async (privateUrl) => {
} catch (err) {
console.log(`Cannot find the file ${privateUrl}`);
}
}
};
const deleteS3 = async (privateUrl) => {
try {
@ -673,7 +695,7 @@ const deleteS3 = async (privateUrl) => {
console.error(`Cannot delete S3 file ${privateUrl}`, error);
throw error;
}
}
};
const uploadStreamToGCloud = async (privateUrl, sourcePath) => {
const { hash, bucket } = initGCloud();
@ -686,7 +708,7 @@ const uploadStreamToGCloud = async (privateUrl, sourcePath) => {
);
return format(`https://storage.googleapis.com/${bucket.name}/${blob.name}`);
}
};
const initUploadSession = async (req, res) => {
try {
@ -755,9 +777,11 @@ const initUploadSession = async (req, res) => {
});
} catch (error) {
console.error('Failed to initialize upload session', error);
return res.status(500).send({ message: 'Failed to initialize upload session' });
return res
.status(500)
.send({ message: 'Failed to initialize upload session' });
}
}
};
const getUploadSession = async (req, res) => {
try {
@ -794,7 +818,7 @@ const getUploadSession = async (req, res) => {
console.error('Failed to get upload session', error);
return res.status(500).send({ message: 'Failed to get upload session' });
}
}
};
const uploadChunk = async (req, res) => {
try {
@ -818,7 +842,12 @@ const uploadChunk = async (req, res) => {
s3Client = s3.client;
s3Bucket = s3.bucket;
s3Prefix = s3.prefix;
session = await readS3SessionMeta(s3Client, s3Bucket, s3Prefix, sessionId);
session = await readS3SessionMeta(
s3Client,
s3Bucket,
s3Prefix,
sessionId,
);
} else {
session = readSessionMeta(sessionId);
}
@ -844,7 +873,14 @@ const uploadChunk = async (req, res) => {
const chunkBuffer = Buffer.concat(chunks);
// Upload chunk directly to S3
await uploadS3Chunk(s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex, chunkBuffer);
await uploadS3Chunk(
s3Client,
s3Bucket,
s3Prefix,
sessionId,
chunkIndex,
chunkBuffer,
);
} else {
// Local storage - write to temp file then rename
const chunkDir = getSessionChunksDir(sessionId);
@ -869,7 +905,13 @@ const uploadChunk = async (req, res) => {
session.updatedAt = new Date().toISOString();
if (provider === 's3') {
await writeS3SessionMeta(s3Client, s3Bucket, s3Prefix, sessionId, session);
await writeS3SessionMeta(
s3Client,
s3Bucket,
s3Prefix,
sessionId,
session,
);
} else {
writeSessionMeta(sessionId, session);
}
@ -884,7 +926,7 @@ const uploadChunk = async (req, res) => {
console.error('Failed to upload chunk', error);
return res.status(500).send({ message: 'Failed to upload chunk' });
}
}
};
const finalizeUploadSession = async (req, res) => {
try {
@ -903,7 +945,12 @@ const finalizeUploadSession = async (req, res) => {
s3Bucket = s3.bucket;
s3Prefix = s3.prefix;
s3Region = s3.region;
session = await readS3SessionMeta(s3Client, s3Bucket, s3Prefix, sessionId);
session = await readS3SessionMeta(
s3Client,
s3Bucket,
s3Prefix,
sessionId,
);
} else {
session = readSessionMeta(sessionId);
}
@ -921,7 +968,13 @@ const finalizeUploadSession = async (req, res) => {
// Verify all chunks exist
if (provider === 's3') {
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
const exists = await s3ChunkExists(s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex);
const exists = await s3ChunkExists(
s3Client,
s3Bucket,
s3Prefix,
sessionId,
chunkIndex,
);
if (!exists) {
return res.status(400).send({
message: `Missing chunk ${chunkIndex}`,
@ -953,7 +1006,13 @@ const finalizeUploadSession = async (req, res) => {
// Download and assemble chunks
if (provider === 's3') {
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
const chunkStream = await downloadS3Chunk(s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex);
const chunkStream = await downloadS3Chunk(
s3Client,
s3Bucket,
s3Prefix,
sessionId,
chunkIndex,
);
if (!chunkStream) {
// Cleanup and return error
if (fs.existsSync(assembledPath)) fs.unlinkSync(assembledPath);
@ -965,7 +1024,9 @@ const finalizeUploadSession = async (req, res) => {
// Write chunk to assembled file
await new Promise((resolve, reject) => {
const writeStream = fs.createWriteStream(assembledPath, { flags: 'a' });
const writeStream = fs.createWriteStream(assembledPath, {
flags: 'a',
});
writeStream.on('error', reject);
writeStream.on('finish', resolve);
@ -973,7 +1034,8 @@ const finalizeUploadSession = async (req, res) => {
chunkStream.on('error', reject);
chunkStream.pipe(writeStream, { end: true });
} else if (typeof chunkStream.transformToByteArray === 'function') {
chunkStream.transformToByteArray()
chunkStream
.transformToByteArray()
.then((bytes) => {
writeStream.write(Buffer.from(bytes));
writeStream.end();
@ -993,7 +1055,10 @@ const finalizeUploadSession = async (req, res) => {
}
const assembledStats = fs.statSync(assembledPath);
if (Number.isFinite(Number(session.size)) && Number(session.size) !== assembledStats.size) {
if (
Number.isFinite(Number(session.size)) &&
Number(session.size) !== assembledStats.size
) {
// Cleanup
if (fs.existsSync(assembledPath)) fs.unlinkSync(assembledPath);
return res.status(400).send({
@ -1042,9 +1107,11 @@ const finalizeUploadSession = async (req, res) => {
});
} catch (error) {
console.error('Failed to finalize upload session', error);
return res.status(500).send({ message: 'Failed to finalize upload session' });
return res
.status(500)
.send({ message: 'Failed to finalize upload session' });
}
}
};
const uploadFile = async (folder, req, res) => {
const provider = getFileStorageProvider();
@ -1061,7 +1128,7 @@ const uploadFile = async (folder, req, res) => {
entity: null,
folderIncludesAuthenticationUid: false,
})(req, res);
}
};
const downloadFile = async (req, res) => {
const provider = getFileStorageProvider();
@ -1075,7 +1142,7 @@ const downloadFile = async (req, res) => {
}
return downloadLocal(req, res);
}
};
const deleteFile = async (privateUrl) => {
const provider = getFileStorageProvider();
@ -1089,7 +1156,7 @@ const deleteFile = async (privateUrl) => {
}
return deleteLocal(privateUrl);
}
};
const PRESIGN_EXPIRY_SECONDS = 3600; // 1 hour
@ -1123,7 +1190,7 @@ const generatePresignedUrls = async (urls) => {
presignedUrls[url] = await getSignedUrl(client, command, {
expiresIn: PRESIGN_EXPIRY_SECONDS,
});
})
}),
);
return presignedUrls;
@ -1150,4 +1217,4 @@ module.exports = {
uploadS3,
downloadS3,
generatePresignedUrls,
}
};

View File

@ -8,8 +8,7 @@ module.exports = class ForbiddenError extends Error {
message = getNotification(messageCode);
}
message =
message || getNotification('errors.forbidden.message');
message = message || getNotification('errors.forbidden.message');
super(message);
this.code = 403;

View File

@ -8,9 +8,7 @@ module.exports = class ValidationError extends Error {
message = getNotification(messageCode);
}
message =
message ||
getNotification('errors.validation.message');
message = message || getNotification('errors.validation.message');
super(message);
this.code = 400;

View File

@ -6,13 +6,8 @@ function format(message, args) {
return null;
}
return message.replace(/{(\d+)}/g, function (
match,
number,
) {
return typeof args[number] != 'undefined'
? args[number]
: match;
return message.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] != 'undefined' ? args[number] : match;
});
}

View File

@ -13,25 +13,22 @@ const errors = {
emailAlreadyInUse: 'Email is already in use',
invalidEmail: 'Please provide a valid email',
passwordReset: {
invalidToken:
'Password reset link is invalid or has expired',
invalidToken: 'Password reset link is invalid or has expired',
error: `Email not recognized`,
},
passwordUpdate: {
samePassword: `You can't use the same password. Please create new password`
samePassword: `You can't use the same password. Please create new password`,
},
userNotVerified: `Sorry, your email has not been verified yet`,
emailAddressVerificationEmail: {
invalidToken:
'Email verification link is invalid or has expired',
invalidToken: 'Email verification link is invalid or has expired',
error: `Email not recognized`,
},
},
iam: {
errors: {
userAlreadyExists:
'User with this email already exists',
userAlreadyExists: 'User with this email already exists',
userNotFound: 'User not found',
disablingHimself: `You can't disable yourself`,
revokingOwnPermission: `You can't revoke your own owner permission`,
@ -43,8 +40,7 @@ const errors = {
importer: {
errors: {
invalidFileEmpty: 'The file is empty',
invalidFileExcel:
'Only excel (.xlsx) files are allowed',
invalidFileExcel: 'Only excel (.xlsx) files are allowed',
invalidFileUpload:
'Invalid file. Make sure you are using the last version of the template.',
importHashRequired: 'Import hash is required',
@ -61,7 +57,7 @@ const errors = {
message: 'An error occurred',
},
searchQueryRequired: {
message: 'Search query is required',
message: 'Search query is required',
},
},

View File

@ -6,8 +6,13 @@ const loadRoleService = () => {
try {
return require('./roles');
} catch (error) {
console.error('Role service is missing. Advanced roles are required for this operation.', error);
const err = new Error('Role service is missing. Advanced roles are required for this operation.');
console.error(
'Role service is missing. Advanced roles are required for this operation.',
error,
);
const err = new Error(
'Role service is missing. Advanced roles are required for this operation.',
);
err.originalError = error;
throw err;
}
@ -35,7 +40,7 @@ module.exports = class OpenAiService {
if (!prompt) {
return {
success: false,
error: 'Prompt is required'
error: 'Prompt is required',
};
}

View File

@ -1,25 +1,18 @@
const db = require('../db/models');
const Project_audio_tracksDBApi = require('../db/api/project_audio_tracks');
const processFile = require("../middlewares/upload");
const processFile = require('../middlewares/upload');
const ValidationError = require('./notifications/errors/validation');
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();
try {
const createdTrack = await Project_audio_tracksDBApi.create(
data,
{
currentUser,
transaction,
},
);
const createdTrack = await Project_audio_tracksDBApi.create(data, {
currentUser,
transaction,
});
await transaction.commit();
return createdTrack;
@ -37,7 +30,7 @@ module.exports = class Project_audio_tracksService {
const bufferStream = new stream.PassThrough();
const results = [];
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream
await new Promise((resolve, reject) => {
bufferStream
@ -48,13 +41,13 @@ module.exports = class Project_audio_tracksService {
resolve();
})
.on('error', (error) => reject(error));
})
});
await Project_audio_tracksDBApi.bulkImport(results, {
transaction,
ignoreDuplicates: true,
validate: true,
currentUser: req.currentUser
transaction,
ignoreDuplicates: true,
validate: true,
currentUser: req.currentUser,
});
await transaction.commit();
@ -68,28 +61,22 @@ module.exports = class Project_audio_tracksService {
const transaction = await db.sequelize.transaction();
try {
let project_audio_tracks = await Project_audio_tracksDBApi.findBy(
{id},
{transaction},
{ id },
{ transaction },
);
if (!project_audio_tracks) {
throw new ValidationError(
'project_audio_tracksNotFound',
);
throw new ValidationError('project_audio_tracksNotFound');
}
const updatedProject_audio_tracks = await Project_audio_tracksDBApi.update(
id,
data,
{
const updatedProject_audio_tracks =
await Project_audio_tracksDBApi.update(id, data, {
currentUser,
transaction,
},
);
});
await transaction.commit();
return updatedProject_audio_tracks;
} catch (error) {
await transaction.rollback();
throw error;
@ -116,13 +103,10 @@ module.exports = class Project_audio_tracksService {
const transaction = await db.sequelize.transaction();
try {
await Project_audio_tracksDBApi.remove(
id,
{
currentUser,
transaction,
},
);
await Project_audio_tracksDBApi.remove(id, {
currentUser,
transaction,
});
await transaction.commit();
} catch (error) {
@ -130,8 +114,4 @@ module.exports = class Project_audio_tracksService {
throw error;
}
}
};

Some files were not shown because too many files have changed in this diff Show More