api/models/site.js
const { Op } = require('sequelize');
const { toInt } = require('../utils');
const {
branchRegex, isEmptyOrUrl, isValidSubdomain,
} = require('../utils/validators');
const afterValidate = (site) => {
if (site.defaultBranch === site.demoBranch) {
const error = new Error('Default branch and demo branch cannot be the same');
error.status = 403;
throw error;
}
if (site.domain && site.domain === site.demoDomain) {
const error = new Error('Domain and demo domain cannot be the same');
error.status = 403;
throw error;
}
};
const validationFailed = (site, options, validationError) => {
const messages = validationError.errors.map(err => `${err.path}: ${err.message}`);
const error = new Error(messages.join('\n'));
error.status = 403;
throw error;
};
const associate = ({
Build,
Domain,
Organization,
OrganizationRole,
Site,
SiteBranchConfig,
SiteUser,
User,
UserAction,
UserEnvironmentVariable,
}) => {
// Associations
Site.hasMany(Build, {
foreignKey: 'site',
});
Site.hasMany(Domain, {
foreignKey: 'siteId',
});
Site.hasMany(SiteBranchConfig, {
foreignKey: 'siteId',
});
Site.belongsToMany(User, {
through: SiteUser,
foreignKey: 'site_users',
timestamps: false,
});
Site.hasMany(UserAction, {
as: 'userActions',
foreignKey: 'siteId',
});
Site.hasMany(UserEnvironmentVariable, {
foreignKey: 'siteId',
});
Site.belongsTo(Organization, {
foreignKey: 'organizationId',
});
// Scopes
Site.addScope('byIdOrText', (search) => {
const query = {};
const id = toInt(search);
if (id) {
query.where = { id };
} else {
query.where = {
[Op.or]: [
{ owner: { [Op.substring]: search } },
{ repository: { [Op.substring]: search } },
],
};
}
return query;
});
Site.addScope('byOrg', id => ({
include: [{
model: Organization,
where: { id },
}],
}));
Site.addScope('forUser', user => ({
where: {
[Op.and]: [
{
[Op.or]: [
{ '$Users.id$': { [Op.not]: null } },
{ organizationId: { [Op.not]: null } },
],
},
{
[Op.or]: [
{ '$Users.id$': user.id },
{ '$Organization.OrganizationRoles.userId$': user.id },
],
},
],
},
include: [
{
model: User,
required: false,
},
{
model: Organization,
required: false,
include: [{
model: OrganizationRole,
}],
},
],
}));
};
const beforeValidate = (site) => {
if (site.repository) {
site.repository = site.repository.toLowerCase(); // eslint-disable-line no-param-reassign
}
if (site.owner) {
site.owner = site.owner.toLowerCase(); // eslint-disable-line no-param-reassign
}
};
function isEmptyOrBranch(value) {
if (value && value.length && !branchRegex.test(value)) {
throw new Error('Invalid branch name — branches can only contain alphanumeric characters, underscores, and hyphens.');
}
}
module.exports = (sequelize, DataTypes) => {
const Site = sequelize.define('Site', {
demoBranch: {
type: DataTypes.STRING,
validate: {
isEmptyOrBranch,
},
},
demoDomain: {
type: DataTypes.STRING,
validate: {
isEmptyOrUrl,
},
},
defaultConfig: {
type: DataTypes.JSONB,
},
defaultBranch: {
type: DataTypes.STRING,
defaultValue: 'master',
validate: {
isEmptyOrBranch,
},
},
domain: {
type: DataTypes.STRING,
validate: {
isEmptyOrUrl,
},
},
engine: {
type: DataTypes.ENUM,
values: ['hugo', 'jekyll', 'node.js', 'static'],
defaultValue: 'static',
},
owner: {
type: DataTypes.STRING,
allowNull: false,
},
previewConfig: {
type: DataTypes.JSONB,
},
demoConfig: {
type: DataTypes.JSONB,
},
publishedAt: {
type: DataTypes.DATE,
},
repository: {
type: DataTypes.STRING,
allowNull: false,
},
repoLastVerified: {
type: DataTypes.DATE,
defaultValue: new Date(),
},
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
s3ServiceName: {
type: DataTypes.STRING,
allowNull: false,
},
awsBucketName: {
type: DataTypes.STRING,
allowNull: false,
},
awsBucketRegion: {
type: DataTypes.STRING,
allowNull: false,
},
config: {
type: DataTypes.JSONB,
defaultValue: {},
},
subdomain: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isValidSubdomain,
},
},
basicAuth: {
type: DataTypes.VIRTUAL,
get() {
return this.config.basicAuth || {};
},
set(basicAuth) {
this.setDataValue('config', { ...this.config, basicAuth });
},
},
containerConfig: {
type: DataTypes.VIRTUAL,
get() {
return this.config.containerConfig || {};
},
set(containerConfig) {
this.setDataValue('config', { ...this.config, containerConfig });
},
},
organizationId: {
type: DataTypes.INTEGER,
references: 'Organization',
},
webhookId: {
type: DataTypes.INTEGER,
},
}, {
tableName: 'site',
hooks: {
beforeValidate,
afterValidate,
validationFailed,
},
paranoid: true,
});
Site.associate = associate;
Site.withUsers = id => Site.findByPk(id, { include: [sequelize.models.User] });
Site.orgScope = id => ({ method: ['byOrg', id] });
Site.searchScope = search => ({ method: ['byIdOrText', search] });
Site.forUser = user => Site.scope({ method: ['forUser', user] });
Site.domainFromContext = context => (context === 'site' ? 'domain' : 'demoDomain');
Site.branchFromContext = context => (context === 'site' ? 'defaultBranch' : 'demoBranch');
return Site;
};