0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor(schemas): init tenant sqls

This commit is contained in:
Gao Sun 2023-01-19 20:27:01 +08:00
parent 53f49df621
commit ba44eb5fc6
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
32 changed files with 362 additions and 93 deletions

View file

@ -13,7 +13,7 @@ import {
managementResourceScope,
defaultRoleScopeRelation,
} from '@logto/schemas';
import { Hooks } from '@logto/schemas/models';
import { Hooks, Tenants } from '@logto/schemas/models';
import chalk from 'chalk';
import type { DatabasePool, DatabaseTransactionConnection } from 'slonik';
import { sql } from 'slonik';
@ -33,6 +33,31 @@ import { getLatestAlterationTimestamp } from '../alteration/index.js';
import { getAlterationDirectory } from '../alteration/utils.js';
import { oidcConfigReaders } from './oidc-config.js';
const getExplicitOrder = (query: string) => {
const matched = /\/\*\s*init_order\s*=\s*(\d+)\s*\*\//.exec(query)?.[1];
return matched ? Number(matched) : undefined;
};
const compareQuery = ([t1, q1]: [string, string], [t2, q2]: [string, string]) => {
const o1 = getExplicitOrder(q1);
const o2 = getExplicitOrder(q2);
if (o1 === undefined && o2 === undefined) {
return t1.localeCompare(t2);
}
if (o1 === undefined) {
return 1;
}
if (o2 === undefined) {
return -1;
}
return o1 - o2;
};
const createTables = async (connection: DatabaseTransactionConnection) => {
const tableDirectory = getPathInModule('@logto/schemas', 'tables');
const directoryFiles = await readdir(tableDirectory);
@ -44,16 +69,19 @@ const createTables = async (connection: DatabaseTransactionConnection) => {
])
);
// Await in loop is intended for better error handling
for (const [, query] of queries) {
console.log(Tenants.raw, getExplicitOrder(Tenants.raw));
const allQueries: Array<[string, string]> = [
[Hooks.tableName, Hooks.raw],
[Tenants.tableName, Tenants.raw],
...queries,
];
const sorted = allQueries.slice().sort(compareQuery);
for (const [, query] of sorted) {
// eslint-disable-next-line no-await-in-loop
await connection.query(sql`${raw(query)}`);
}
for (const table of [Hooks]) {
// eslint-disable-next-line no-await-in-loop
await connection.query(sql`${raw(table.raw)}`);
}
};
const seedTables = async (connection: DatabaseTransactionConnection, latestTimestamp: number) => {
@ -133,6 +161,7 @@ export const seedByPool = async (pool: DatabasePool, type: SeedChoice) => {
);
}
await createTables(connection);
await oraPromise(createTables(connection), {
text: 'Create tables',
prefixText: chalk.blue('[info]'),

View file

@ -0,0 +1,140 @@
import { conditionalString } from '@silverhand/essentials';
import { sql } from 'slonik';
import { raw } from 'slonik-sql-tag-raw';
import type { AlterationScript } from '../lib/types/alteration.js';
const getId = (value: string) => sql.identifier([value]);
const tenantId = sql.identifier(['tenant_id']);
const defaultTenantId = 'default';
// [table name, primary key array]
type TableInfo = [string, string[]];
const tables: TableInfo[] = [
['applications', ['id']],
['connectors', ['id']],
['custom_phrases', ['language_tag']],
['logs', ['id']],
['oidc_model_instances', ['model_name', 'id']],
['passcodes', ['id']],
['resources', ['id']],
['roles', ['id']],
['roles_scopes', ['role_id', 'scope_id']],
['scopes', ['id']],
['settings', ['id']],
['sign_in_experiences', ['id']],
['users_roles', ['user_id', 'role_id']],
['users', ['id']],
];
type IndexInfo = {
table: string;
indexes: Array<{ name?: string; type?: 'unique'; columns: string[]; strategy?: 'drop-only' }>;
};
const indexes: IndexInfo[] = [
{
table: 'logs',
indexes: [
{ columns: ['key'] },
{ columns: ['created_at'], strategy: 'drop-only' },
{ name: 'user_id', columns: ["(payload->>'user_id') nulls last"] },
{ name: 'application_id', columns: ["(payload->>'application_id') nulls last"] },
],
},
{
table: 'oidc_model_instances',
indexes: [
{ name: 'model_name_payload_user_code', columns: ['model_name', "(payload->>'userCode')"] },
{ name: 'model_name_payload_uid', columns: ['model_name', "(payload->>'uid')"] },
{ name: 'model_name_payload_grant_id', columns: ['model_name', "(payload->>'grantId')"] },
],
},
{
table: 'passcodes',
indexes: [
{ columns: ['interaction_jti', 'type'] },
{ columns: ['email', 'type'] },
{ columns: ['phone', 'type'] },
],
},
{ table: 'resources', indexes: [{ type: 'unique', columns: ['indicator'] }] },
{ table: 'roles', indexes: [{ type: 'unique', columns: ['name'] }] },
{ table: 'scopes', indexes: [{ type: 'unique', columns: ['resource_id', 'name'] }] },
{
table: 'users',
indexes: [{ columns: ['name'] }, { columns: ['created_at'], strategy: 'drop-only' }],
},
];
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
create table tenants (
id varchar(21) not null,
db_user_password varchar(128),
primary key (id)
);
`);
await pool.query(sql`
insert into tenants (${getId('id')}, ${getId('db_user_password')})
values (${defaultTenantId}, null);
`);
// Update primary keys
await Promise.all(
tables.map(async ([tableName, primaryKeys]) => {
// Add `tenant_id` column and set existing data to a default tenant
await pool.query(sql`
alter table ${sql.identifier([tableName])}
add column ${tenantId} varchar(21) not null default 'default'
references tenants (id) on update cascade on delete cascade,
drop constraint ${sql.identifier([tableName + '_pkey'])} cascade,
add primary key (${sql.join(
['tenant_id', ...primaryKeys].map((key) => sql.identifier([key])),
sql`, `
)});
`);
// Column should not have a default tenant ID, it should be always manually assigned
await pool.query(sql`
alter table ${sql.identifier([tableName])}
alter column ${tenantId} drop default;
`);
})
);
// Update indexes
await Promise.all(
indexes.flatMap(({ table, indexes }) =>
indexes.map(async ({ name, type, columns, strategy }) => {
const indexName = getId(`${table}__${name ?? columns.join('_')}`);
await pool.query(sql`drop index ${indexName}`);
if (strategy !== 'drop-only') {
await pool.query(
sql`
create ${raw(conditionalString(type))} index ${indexName}
on ${getId(table)}
(
${tenantId},
${sql.join(
columns.map((column) => raw(column)),
sql`, `
)}
);
`
);
}
})
)
);
},
down: async (pool) => {
throw new Error('Not implemented');
},
};
export default alteration;

View file

@ -52,7 +52,9 @@
"lint-staged": "^13.0.0",
"pluralize": "^8.0.0",
"prettier": "^2.8.1",
"roarr": "^7.11.0",
"slonik": "^30.0.0",
"slonik-sql-tag-raw": "^1.1.4",
"typescript": "^4.9.4"
},
"eslintConfig": {

View file

@ -44,8 +44,8 @@ const generate = async () => {
// Get statements
const statements = paragraph
.split(';')
.map((value) => normalizeWhitespaces(value))
.map((value) => removeUnrecognizedComments(value));
.map((value) => removeUnrecognizedComments(value))
.map((value) => normalizeWhitespaces(value));
// Parse Table statements
const tables = statements

View file

@ -1,6 +1,7 @@
import { createModel } from '@withtyped/server';
export const Tenants = createModel(/* sql */ `
/* init_order = 0 */
create table tenants (
id varchar(32) not null,
db_user_password varchar(128) not null,

View file

@ -1,5 +1,6 @@
import type { CreateApplication } from '../db-entries/index.js';
import { ApplicationType } from '../db-entries/index.js';
import { defaultTenantId } from './tenant.js';
/**
* The fixed application ID for Admin Console.
@ -11,6 +12,7 @@ export const adminConsoleApplicationId = 'admin-console';
export const demoAppApplicationId = 'demo-app';
export const createDemoAppApplication = (secret: string): Readonly<CreateApplication> => ({
tenantId: defaultTenantId,
id: demoAppApplicationId,
secret,
name: 'Demo App',

View file

@ -1,8 +1,10 @@
import type { CreateResource } from '../db-entries/index.js';
import { defaultTenantId } from './tenant.js';
export const managementResourceId = 'management-api';
export const managementResource: Readonly<CreateResource> = Object.freeze({
tenantId: defaultTenantId,
id: managementResourceId,
/**
* The fixed resource indicator for Management APIs.

View file

@ -1,6 +1,7 @@
import type { CreateRole, CreateRolesScope } from '../db-entries/index.js';
import { UserRole } from '../types/index.js';
import { managementResourceScopeId } from './scope.js';
import { defaultTenantId } from './tenant.js';
export const adminConsoleAdminRoleId = 'ac-admin-id';
@ -8,12 +9,14 @@ export const adminConsoleAdminRoleId = 'ac-admin-id';
* Default Admin Role for Admin Console.
*/
export const defaultRole: Readonly<CreateRole> = {
tenantId: defaultTenantId,
id: adminConsoleAdminRoleId,
name: UserRole.Admin,
description: 'Admin role for Logto.',
};
export const defaultRoleScopeRelation: Readonly<CreateRolesScope> = {
tenantId: defaultTenantId,
roleId: adminConsoleAdminRoleId,
scopeId: managementResourceScopeId,
};

View file

@ -1,9 +1,11 @@
import type { CreateScope } from '../db-entries/index.js';
import { managementResourceId } from './resource.js';
import { defaultTenantId } from './tenant.js';
export const managementResourceScopeId = 'management-api-scope';
export const managementResourceScope: Readonly<CreateScope> = Object.freeze({
tenantId: defaultTenantId,
id: managementResourceScopeId,
name: 'management-api:default',
description: 'Default scope for management API',

View file

@ -1,10 +1,12 @@
import type { CreateSetting } from '../db-entries/index.js';
import { AppearanceMode } from '../foundations/index.js';
import { defaultTenantId } from './tenant.js';
export const defaultSettingId = 'default';
export const createDefaultSetting = (): Readonly<CreateSetting> =>
Object.freeze({
tenantId: defaultTenantId,
id: defaultSettingId,
adminConsole: {
language: 'en',

View file

@ -3,10 +3,12 @@ import { generateDarkColor } from '@logto/core-kit';
import type { CreateSignInExperience } from '../db-entries/index.js';
import { SignInMode } from '../db-entries/index.js';
import { BrandingStyle, SignInIdentifier } from '../foundations/index.js';
import { defaultTenantId } from './tenant.js';
const defaultPrimaryColor = '#6139F6';
export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
tenantId: defaultTenantId,
id: 'default',
color: {
primaryColor: defaultPrimaryColor,
@ -52,5 +54,6 @@ export const adminConsoleSignInExperience: CreateSignInExperience = {
style: BrandingStyle.Logo_Slogan,
logoUrl: 'https://logto.io/logo.svg',
darkLogoUrl: 'https://logto.io/logo-dark.svg',
slogan: 'admin_console.welcome.title', // TODO: @simeng should we programmatically support an i18n key for slogan?
},
};

View file

@ -0,0 +1,2 @@
export const defaultTenantId = 'default';
export const adminTenantId = 'admin';

View file

@ -1,6 +1,10 @@
/* init_order = 1 */
create type application_type as enum ('Native', 'SPA', 'Traditional', 'MachineToMachine');
create table applications (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
name varchar(256) not null,
secret varchar(64) not null,
@ -11,3 +15,6 @@ create table applications (
created_at timestamptz not null default(now()),
primary key (id)
);
create index applications__id
on applications (tenant_id, id)

View file

@ -0,0 +1,15 @@
create table applications_roles (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
application_id varchar(21) not null
references applications (id) on update cascade on delete cascade,
role_id varchar(21) not null
references roles (id) on update cascade on delete cascade,
primary key (id),
constraint applications_roles__application_id_role_id
unique (tenant_id, application_id, role_id)
);
create index applications_roles__id
on applications_roles (tenant_id, id);

View file

@ -1,4 +1,6 @@
create table connectors (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(128) not null,
sync_profile boolean not null default FALSE,
connector_id varchar(128) not null,
@ -7,3 +9,6 @@ create table connectors (
created_at timestamptz not null default(now()),
primary key (id)
);
create index connectors__id
on connectors (tenant_id, id);

View file

@ -1,5 +1,13 @@
create table custom_phrases (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
language_tag varchar(16) not null,
translation jsonb /* @use Translation */ not null,
primary key(language_tag)
primary key (id),
constraint custom_phrases__language_tag
unique (tenant_id, language_tag)
);
create index custom_phrases__id
on custom_phrases (tenant_id, id);

View file

@ -1,5 +1,6 @@
create table logs
(
create table logs (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
key varchar(128) not null,
payload jsonb /* @use LogContextPayload */ not null default '{}'::jsonb,
@ -7,7 +8,14 @@ create table logs
primary key (id)
);
create index logs__key on logs (key);
create index logs__created_at on logs (created_at);
create index logs__user_id on logs ((payload->>'user_id') nulls last);
create index logs__application_id on logs ((payload->>'application_id') nulls last);
create index logs__id
on logs (tenant_id, id);
create index logs__key
on logs (tenant_id, key);
create index logs__user_id
on logs (tenant_id, (payload->>'user_id') nulls last);
create index logs__application_id
on logs (tenant_id, (payload->>'application_id') nulls last);

View file

@ -1,26 +1,34 @@
create table oidc_model_instances (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
model_name varchar(64) not null,
id varchar(128) not null,
payload jsonb /* @use OidcModelInstancePayload */ not null,
expires_at timestamptz not null,
consumed_at timestamptz,
primary key (model_name, id)
primary key (id)
);
create index oidc_model_instances__model_name_id
on oidc_model_instances (tenant_id, model_name, id);
create index oidc_model_instances__model_name_payload_user_code
on oidc_model_instances (
on oidc_model_instances (
tenant_id,
model_name,
(payload->>'userCode')
);
);
create index oidc_model_instances__model_name_payload_uid
on oidc_model_instances (
on oidc_model_instances (
tenant_id,
model_name,
(payload->>'uid')
);
);
create index oidc_model_instances__model_name_payload_grant_id
on oidc_model_instances (
on oidc_model_instances (
tenant_id,
model_name,
(payload->>'grantId')
);
);

View file

@ -1,4 +1,6 @@
create table passcodes (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
interaction_jti varchar(128),
phone varchar(32),
@ -11,20 +13,14 @@ create table passcodes (
primary key (id)
);
create index passcodes__id
on passcodes (tenant_id, id);
create index passcodes__interaction_jti_type
on passcodes (
interaction_jti,
type
);
on passcodes (tenant_id, interaction_jti, type);
create index passcodes__email_type
on passcodes (
email,
type
);
on passcodes (tenant_id, email, type);
create index passcodes__phone_type
on passcodes (
phone,
type
);
on passcodes (tenant_id, phone, type);

View file

@ -1,12 +1,14 @@
create table resources (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
name text not null,
indicator text not null unique, /* resource indicator also used as audience */
access_token_ttl bigint not null default(3600), /* expiration value in seconds, default is 1h */
primary key (id)
primary key (id),
constraint resources__indicator
unique (tenant_id, indicator)
);
create unique index resources__indicator
on resources (
indicator
);
create index resources__id
on resources (tenant_id, indicator);

View file

@ -1,11 +1,15 @@
/* init_order = 1 */
create table roles (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
name varchar(128) not null,
description varchar(128) not null,
primary key (id)
primary key (id),
constraint roles__name
unique (tenant_id, name)
);
create unique index roles__name
on roles (
name
);
create index roles__id
on roles (tenant_id, id);

View file

@ -0,0 +1,15 @@
create table roles_scopes (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
role_id varchar(21) not null
references roles (id) on update cascade on delete cascade,
scope_id varchar(21) not null
references scopes (id) on update cascade on delete cascade,
primary key (id),
constraint roles_scopes__role_id_scope_id
unique (tenant_id, role_id, scope_id)
);
create index roles_scopes__id
on roles_scopes (tenant_id, id);

View file

@ -1,5 +0,0 @@
create table applications_roles (
application_id varchar(21) not null references applications (id) on update cascade on delete cascade,
role_id varchar(21) not null references roles (id) on update cascade on delete cascade,
primary key (application_id, role_id)
);

View file

@ -1,14 +1,16 @@
create table scopes (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
resource_id varchar(21) not null references resources (id) on update cascade on delete cascade,
resource_id varchar(21) not null
references resources (id) on update cascade on delete cascade,
name varchar(256) not null,
description text not null,
created_at timestamptz not null default(now()),
primary key (id)
primary key (id),
constraint scopes__resource_id_name
unique (tenant_id, resource_id, name)
);
create index scopes__resource_id_name
on scopes (
resource_id,
name
);
create index scopes__id
on scopes (tenant_id, id);

View file

@ -1,5 +0,0 @@
create table roles_scopes (
role_id varchar(21) not null references roles (id) on update cascade on delete cascade,
scope_id varchar(21) not null references scopes (id) on update cascade on delete cascade,
primary key (role_id, scope_id)
);

View file

@ -1,5 +1,10 @@
create table settings (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
admin_console jsonb /* @use AdminConsoleConfig */ not null,
primary key (id)
);
create index settings__id
on settings (tenant_id, id);

View file

@ -1,6 +1,8 @@
create type sign_in_mode as enum ('SignIn', 'Register', 'SignInAndRegister');
create table sign_in_experiences (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(21) not null,
color jsonb /* @use Color */ not null,
branding jsonb /* @use Branding */ not null,
@ -12,3 +14,6 @@ create table sign_in_experiences (
sign_in_mode sign_in_mode not null default 'SignInAndRegister',
primary key (id)
);
create index sign_in_experiences__id
on sign_in_experiences (tenant_id, id);

View file

@ -1,6 +1,10 @@
/* init_order = 1 */
create type users_password_encryption_method as enum ('Argon2i');
create table users (
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
id varchar(12) not null,
username varchar(128) unique,
primary_email varchar(128) unique,
@ -18,5 +22,8 @@ create table users (
primary key (id)
);
create index users__created_at on users (created_at);
create index users__name on users (name);
create index users__id
on users (tenant_id, id);
create index users__name
on users (tenant_id, name);

View file

@ -0,0 +1,14 @@
create table users_roles (
id varchar(21) not null,
tenant_id varchar(21) not null
references tenants (id) on update cascade on delete cascade,
user_id varchar(21) not null
references users (id) on update cascade on delete cascade,
role_id varchar(21) not null
references roles (id) on update cascade on delete cascade,
primary key (id),
constraint users_roles__user_id_role_id unique (tenant_id, user_id, role_id)
);
create index users_roles__id
on users_roles (tenant_id, id);

View file

@ -1,5 +0,0 @@
create table users_roles (
user_id varchar(21) not null references users (id) on update cascade on delete cascade,
role_id varchar(21) not null references roles (id) on update cascade on delete cascade,
primary key (user_id, role_id)
);

View file

@ -590,7 +590,9 @@ importers:
lint-staged: ^13.0.0
pluralize: ^8.0.0
prettier: ^2.8.1
roarr: ^7.11.0
slonik: ^30.0.0
slonik-sql-tag-raw: ^1.1.4
typescript: ^4.9.4
zod: ^3.20.2
dependencies:
@ -614,7 +616,9 @@ importers:
lint-staged: 13.0.0
pluralize: 8.0.0
prettier: 2.8.1
roarr: 7.11.0
slonik: 30.1.2
slonik-sql-tag-raw: 1.1.4_roarr@7.11.0+slonik@30.1.2
typescript: 4.9.4
packages/shared:
@ -5893,19 +5897,12 @@ packages:
engines: {node: '>=8'}
dev: true
/define-properties/1.1.3:
resolution: {integrity: sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==}
engines: {node: '>= 0.4'}
dependencies:
object-keys: 1.1.1
/define-properties/1.1.4:
resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
engines: {node: '>= 0.4'}
dependencies:
has-property-descriptors: 1.0.0
object-keys: 1.1.1
dev: true
/delay/4.4.1:
resolution: {integrity: sha512-aL3AhqtfhOlT/3ai6sWXeqwnw63ATNpnUiN4HL7x9q+My5QtHlO3OIkasmug9LKzpheLdmUKGRKnYXYAS7FQkQ==}
@ -7388,7 +7385,7 @@ packages:
resolution: {integrity: sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==}
engines: {node: '>= 0.4'}
dependencies:
define-properties: 1.1.3
define-properties: 1.1.4
/globalyzer/0.1.0:
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
@ -7486,7 +7483,6 @@ packages:
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
dependencies:
get-intrinsic: 1.1.3
dev: true
/has-symbols/1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
@ -13177,7 +13173,6 @@ packages:
roarr: 7.11.0
serialize-error: 8.1.0
slonik: 30.1.2
dev: false
/slonik/22.7.1:
resolution: {integrity: sha512-88GidNOWv4Bg0CqYLXajqcD0bbLip2soY6B4JzHP7EGDrWUb1WSlu7mIppTJVfcK99mx+jnX3xQq3FJ0DoOXag==}