mirror of
https://github.com/immich-app/immich.git
synced 2025-06-05 06:35:07 -04:00
feat: extension, triggers, functions, comments, parameters management in sql-tools (#17269)
feat: sql-tools extension, triggers, functions, comments, parameters
This commit is contained in:
parent
51c2c60231
commit
e7a5b96ed0
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -518,7 +518,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run existing migrations
|
- name: Run existing migrations
|
||||||
run: npm run typeorm:migrations:run
|
run: npm run migrations:run
|
||||||
|
|
||||||
- name: Test npm run schema:reset command works
|
- name: Test npm run schema:reset command works
|
||||||
run: npm run typeorm:schema:reset
|
run: npm run typeorm:schema:reset
|
||||||
@ -532,7 +532,7 @@ jobs:
|
|||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
server/src/migrations/
|
server/src
|
||||||
- name: Verify migration files have not changed
|
- name: Verify migration files have not changed
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
|
@ -25,10 +25,10 @@
|
|||||||
"lifecycle": "node ./dist/utils/lifecycle.js",
|
"lifecycle": "node ./dist/utils/lifecycle.js",
|
||||||
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
||||||
"migrations:create": "node ./dist/bin/migrations.js create",
|
"migrations:create": "node ./dist/bin/migrations.js create",
|
||||||
"typeorm:migrations:run": "typeorm migration:run -d ./dist/bin/database.js",
|
"migrations:run": "node ./dist/bin/migrations.js run",
|
||||||
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js",
|
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js",
|
||||||
"typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'",
|
"typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'",
|
||||||
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
|
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run migrations:run",
|
||||||
"kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts",
|
"kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts",
|
||||||
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
||||||
"sync:sql": "node ./dist/bin/sync-sql.js",
|
"sync:sql": "node ./dist/bin/sync-sql.js",
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
process.env.DB_URL = 'postgres://postgres:postgres@localhost:5432/immich';
|
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
|
||||||
|
|
||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||||
import { writeFileSync } from 'node:fs';
|
import { writeFileSync } from 'node:fs';
|
||||||
|
import { basename, dirname, extname, join } from 'node:path';
|
||||||
import postgres from 'postgres';
|
import postgres from 'postgres';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import 'src/schema/tables';
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
import { DatabaseTable, schemaDiff, schemaFromDatabase, schemaFromDecorators } from 'src/sql-tools';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import 'src/schema';
|
||||||
|
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const command = process.argv[2];
|
const command = process.argv[2];
|
||||||
const name = process.argv[3] || 'Migration';
|
const path = process.argv[3] || 'src/Migration';
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'debug': {
|
case 'debug': {
|
||||||
@ -17,13 +22,19 @@ const main = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'run': {
|
||||||
|
const only = process.argv[3] as 'kysely' | 'typeorm' | undefined;
|
||||||
|
await run(only);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case 'create': {
|
case 'create': {
|
||||||
create(name, [], []);
|
create(path, [], []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'generate': {
|
case 'generate': {
|
||||||
await generate(name);
|
await generate(path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,32 +42,57 @@ const main = async () => {
|
|||||||
console.log(`Usage:
|
console.log(`Usage:
|
||||||
node dist/bin/migrations.js create <name>
|
node dist/bin/migrations.js create <name>
|
||||||
node dist/bin/migrations.js generate <name>
|
node dist/bin/migrations.js generate <name>
|
||||||
|
node dist/bin/migrations.js run
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const run = async (only?: 'kysely' | 'typeorm') => {
|
||||||
|
const configRepository = new ConfigRepository();
|
||||||
|
const { database } = configRepository.getEnv();
|
||||||
|
const logger = new LoggingRepository(undefined, configRepository);
|
||||||
|
const db = new Kysely<any>({
|
||||||
|
dialect: new PostgresJSDialect({ postgres: postgres(database.config.kysely) }),
|
||||||
|
log(event) {
|
||||||
|
if (event.level === 'error') {
|
||||||
|
console.error('Query failed :', {
|
||||||
|
durationMs: event.queryDurationMillis,
|
||||||
|
error: event.error,
|
||||||
|
sql: event.query.sql,
|
||||||
|
params: event.query.parameters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
|
||||||
|
|
||||||
|
await databaseRepository.runMigrations({ only });
|
||||||
|
};
|
||||||
|
|
||||||
const debug = async () => {
|
const debug = async () => {
|
||||||
const { up, down } = await compare();
|
const { up } = await compare();
|
||||||
const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n');
|
const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n');
|
||||||
const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n');
|
// const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n');
|
||||||
writeFileSync('./migrations.sql', upSql + '\n\n' + downSql);
|
writeFileSync('./migrations.sql', upSql + '\n\n');
|
||||||
console.log('Wrote migrations.sql');
|
console.log('Wrote migrations.sql');
|
||||||
};
|
};
|
||||||
|
|
||||||
const generate = async (name: string) => {
|
const generate = async (path: string) => {
|
||||||
const { up, down } = await compare();
|
const { up, down } = await compare();
|
||||||
if (up.items.length === 0) {
|
if (up.items.length === 0) {
|
||||||
console.log('No changes detected');
|
console.log('No changes detected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
create(name, up.asSql(), down.asSql());
|
create(path, up.asSql(), down.asSql());
|
||||||
};
|
};
|
||||||
|
|
||||||
const create = (name: string, up: string[], down: string[]) => {
|
const create = (path: string, up: string[], down: string[]) => {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
|
const name = basename(path, extname(path));
|
||||||
const filename = `${timestamp}-${name}.ts`;
|
const filename = `${timestamp}-${name}.ts`;
|
||||||
const fullPath = `./src/${filename}`;
|
const folder = dirname(path);
|
||||||
|
const fullPath = join(folder, filename);
|
||||||
writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down }));
|
writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down }));
|
||||||
console.log(`Wrote ${fullPath}`);
|
console.log(`Wrote ${fullPath}`);
|
||||||
};
|
};
|
||||||
@ -66,16 +102,25 @@ const compare = async () => {
|
|||||||
const { database } = configRepository.getEnv();
|
const { database } = configRepository.getEnv();
|
||||||
const db = postgres(database.config.kysely);
|
const db = postgres(database.config.kysely);
|
||||||
|
|
||||||
const source = schemaFromDecorators();
|
const source = schemaFromCode();
|
||||||
const target = await schemaFromDatabase(db, {});
|
const target = await schemaFromDatabase(db, {});
|
||||||
|
|
||||||
|
const sourceParams = new Set(source.parameters.map(({ name }) => name));
|
||||||
|
target.parameters = target.parameters.filter(({ name }) => sourceParams.has(name));
|
||||||
|
|
||||||
|
const sourceTables = new Set(source.tables.map(({ name }) => name));
|
||||||
|
target.tables = target.tables.filter(({ name }) => sourceTables.has(name));
|
||||||
|
|
||||||
console.log(source.warnings.join('\n'));
|
console.log(source.warnings.join('\n'));
|
||||||
|
|
||||||
const isIncluded = (table: DatabaseTable) => source.tables.some(({ name }) => table.name === name);
|
const up = schemaDiff(source, target, {
|
||||||
target.tables = target.tables.filter((table) => isIncluded(table));
|
tables: { ignoreExtra: true },
|
||||||
|
functions: { ignoreExtra: false },
|
||||||
const up = schemaDiff(source, target, { ignoreExtraTables: true });
|
});
|
||||||
const down = schemaDiff(target, source, { ignoreExtraTables: false });
|
const down = schemaDiff(target, source, {
|
||||||
|
tables: { ignoreExtra: false },
|
||||||
|
functions: { ignoreExtra: false },
|
||||||
|
});
|
||||||
|
|
||||||
return { up, down };
|
return { up, down };
|
||||||
};
|
};
|
||||||
|
@ -4,8 +4,24 @@ import _ from 'lodash';
|
|||||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||||
import { ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
|
import { ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
|
||||||
import { EmitEvent } from 'src/repositories/event.repository';
|
import { EmitEvent } from 'src/repositories/event.repository';
|
||||||
|
import { immich_uuid_v7, updated_at } from 'src/schema/functions';
|
||||||
|
import { BeforeUpdateTrigger, Column, ColumnOptions } from 'src/sql-tools';
|
||||||
import { setUnion } from 'src/utils/set';
|
import { setUnion } from 'src/utils/set';
|
||||||
|
|
||||||
|
const GeneratedUuidV7Column = (options: Omit<ColumnOptions, 'type' | 'default' | 'nullable'> = {}) =>
|
||||||
|
Column({ ...options, type: 'uuid', nullable: false, default: () => `${immich_uuid_v7.name}()` });
|
||||||
|
|
||||||
|
export const UpdateIdColumn = () => GeneratedUuidV7Column();
|
||||||
|
|
||||||
|
export const PrimaryGeneratedUuidV7Column = () => GeneratedUuidV7Column({ primary: true });
|
||||||
|
|
||||||
|
export const UpdatedAtTrigger = (name: string) =>
|
||||||
|
BeforeUpdateTrigger({
|
||||||
|
name,
|
||||||
|
scope: 'row',
|
||||||
|
function: updated_at,
|
||||||
|
});
|
||||||
|
|
||||||
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
||||||
// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching
|
// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching
|
||||||
// by a list of IDs) requires splitting the query into multiple chunks.
|
// by a list of IDs) requires splitting the query into multiple chunks.
|
||||||
|
12
server/src/migrations/1743595393000-TableCleanup.ts
Normal file
12
server/src/migrations/1743595393000-TableCleanup.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class TableCleanup1743595393000 implements MigrationInterface {
|
||||||
|
name = 'TableCleanup1743595393000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS "system_config"`);
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS "socket_io_attachments"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(): Promise<void> {}
|
||||||
|
}
|
@ -197,58 +197,62 @@ export class DatabaseRepository {
|
|||||||
return dimSize;
|
return dimSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void> {
|
async runMigrations(options?: { transaction?: 'all' | 'none' | 'each'; only?: 'kysely' | 'typeorm' }): Promise<void> {
|
||||||
const { database } = this.configRepository.getEnv();
|
const { database } = this.configRepository.getEnv();
|
||||||
const dataSource = new DataSource(database.config.typeorm);
|
if (options?.only !== 'kysely') {
|
||||||
|
const dataSource = new DataSource(database.config.typeorm);
|
||||||
|
|
||||||
this.logger.log('Running migrations, this may take a while');
|
this.logger.log('Running migrations, this may take a while');
|
||||||
|
|
||||||
this.logger.debug('Running typeorm migrations');
|
this.logger.debug('Running typeorm migrations');
|
||||||
|
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
await dataSource.runMigrations(options);
|
await dataSource.runMigrations(options);
|
||||||
await dataSource.destroy();
|
await dataSource.destroy();
|
||||||
|
|
||||||
this.logger.debug('Finished running typeorm migrations');
|
this.logger.debug('Finished running typeorm migrations');
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/prefer-module
|
|
||||||
const migrationFolder = join(__dirname, '..', 'schema/migrations');
|
|
||||||
|
|
||||||
// TODO remove after we have at least one kysely migration
|
|
||||||
if (!existsSync(migrationFolder)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Running kysely migrations');
|
if (options?.only !== 'typeorm') {
|
||||||
const migrator = new Migrator({
|
// eslint-disable-next-line unicorn/prefer-module
|
||||||
db: this.db,
|
const migrationFolder = join(__dirname, '..', 'schema/migrations');
|
||||||
migrationLockTableName: 'kysely_migrations_lock',
|
|
||||||
migrationTableName: 'kysely_migrations',
|
|
||||||
provider: new FileMigrationProvider({
|
|
||||||
fs: { readdir },
|
|
||||||
path: { join },
|
|
||||||
migrationFolder,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { error, results } = await migrator.migrateToLatest();
|
// TODO remove after we have at least one kysely migration
|
||||||
|
if (!existsSync(migrationFolder)) {
|
||||||
for (const result of results ?? []) {
|
return;
|
||||||
if (result.status === 'Success') {
|
|
||||||
this.logger.log(`Migration "${result.migrationName}" succeeded`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.status === 'Error') {
|
this.logger.debug('Running kysely migrations');
|
||||||
this.logger.warn(`Migration "${result.migrationName}" failed`);
|
const migrator = new Migrator({
|
||||||
|
db: this.db,
|
||||||
|
migrationLockTableName: 'kysely_migrations_lock',
|
||||||
|
migrationTableName: 'kysely_migrations',
|
||||||
|
provider: new FileMigrationProvider({
|
||||||
|
fs: { readdir },
|
||||||
|
path: { join },
|
||||||
|
migrationFolder,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { error, results } = await migrator.migrateToLatest();
|
||||||
|
|
||||||
|
for (const result of results ?? []) {
|
||||||
|
if (result.status === 'Success') {
|
||||||
|
this.logger.log(`Migration "${result.migrationName}" succeeded`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 'Error') {
|
||||||
|
this.logger.warn(`Migration "${result.migrationName}" failed`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
this.logger.error(`Kysely migrations failed: ${error}`);
|
this.logger.error(`Kysely migrations failed: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Finished running kysely migrations');
|
this.logger.debug('Finished running kysely migrations');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async withLock<R>(lock: DatabaseLock, callback: () => Promise<R>): Promise<R> {
|
async withLock<R>(lock: DatabaseLock, callback: () => Promise<R>): Promise<R> {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
import { ConsoleLogger, Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
import { Telemetry } from 'src/decorators';
|
import { Telemetry } from 'src/decorators';
|
||||||
@ -26,7 +26,7 @@ export class MyConsoleLogger extends ConsoleLogger {
|
|||||||
private isColorEnabled: boolean;
|
private isColorEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cls: ClsService,
|
private cls: ClsService | undefined,
|
||||||
options?: { color?: boolean; context?: string },
|
options?: { color?: boolean; context?: string },
|
||||||
) {
|
) {
|
||||||
super(options?.context || MyConsoleLogger.name);
|
super(options?.context || MyConsoleLogger.name);
|
||||||
@ -74,7 +74,7 @@ export class MyConsoleLogger extends ConsoleLogger {
|
|||||||
export class LoggingRepository {
|
export class LoggingRepository {
|
||||||
private logger: MyConsoleLogger;
|
private logger: MyConsoleLogger;
|
||||||
|
|
||||||
constructor(cls: ClsService, configRepository: ConfigRepository) {
|
constructor(@Inject(ClsService) cls: ClsService | undefined, configRepository: ConfigRepository) {
|
||||||
const { noColor } = configRepository.getEnv();
|
const { noColor } = configRepository.getEnv();
|
||||||
this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor });
|
this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor });
|
||||||
}
|
}
|
||||||
|
12
server/src/schema/enums.ts
Normal file
12
server/src/schema/enums.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { AssetStatus, SourceType } from 'src/enum';
|
||||||
|
import { registerEnum } from 'src/sql-tools';
|
||||||
|
|
||||||
|
export const assets_status_enum = registerEnum({
|
||||||
|
name: 'assets_status_enum',
|
||||||
|
values: Object.values(AssetStatus),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const asset_face_source_type = registerEnum({
|
||||||
|
name: 'sourcetype',
|
||||||
|
values: Object.values(SourceType),
|
||||||
|
});
|
116
server/src/schema/functions.ts
Normal file
116
server/src/schema/functions.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { registerFunction } from 'src/sql-tools';
|
||||||
|
|
||||||
|
export const immich_uuid_v7 = registerFunction({
|
||||||
|
name: 'immich_uuid_v7',
|
||||||
|
arguments: ['p_timestamp timestamp with time zone default clock_timestamp()'],
|
||||||
|
returnType: 'uuid',
|
||||||
|
language: 'SQL',
|
||||||
|
behavior: 'volatile',
|
||||||
|
body: `
|
||||||
|
SELECT encode(
|
||||||
|
set_bit(
|
||||||
|
set_bit(
|
||||||
|
overlay(uuid_send(gen_random_uuid())
|
||||||
|
placing substring(int8send(floor(extract(epoch from p_timestamp) * 1000)::bigint) from 3)
|
||||||
|
from 1 for 6
|
||||||
|
),
|
||||||
|
52, 1
|
||||||
|
),
|
||||||
|
53, 1
|
||||||
|
),
|
||||||
|
'hex')::uuid;
|
||||||
|
`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updated_at = registerFunction({
|
||||||
|
name: 'updated_at',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
DECLARE
|
||||||
|
clock_timestamp TIMESTAMP := clock_timestamp();
|
||||||
|
BEGIN
|
||||||
|
new."updatedAt" = clock_timestamp;
|
||||||
|
new."updateId" = immich_uuid_v7(clock_timestamp);
|
||||||
|
return new;
|
||||||
|
END;`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const f_concat_ws = registerFunction({
|
||||||
|
name: 'f_concat_ws',
|
||||||
|
arguments: ['text', 'text[]'],
|
||||||
|
returnType: 'text',
|
||||||
|
language: 'SQL',
|
||||||
|
parallel: 'safe',
|
||||||
|
behavior: 'immutable',
|
||||||
|
body: `SELECT array_to_string($2, $1)`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const f_unaccent = registerFunction({
|
||||||
|
name: 'f_unaccent',
|
||||||
|
arguments: ['text'],
|
||||||
|
returnType: 'text',
|
||||||
|
language: 'SQL',
|
||||||
|
parallel: 'safe',
|
||||||
|
strict: true,
|
||||||
|
behavior: 'immutable',
|
||||||
|
return: `unaccent('unaccent', $1)`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ll_to_earth_public = registerFunction({
|
||||||
|
name: 'll_to_earth_public',
|
||||||
|
arguments: ['latitude double precision', 'longitude double precision'],
|
||||||
|
returnType: 'public.earth',
|
||||||
|
language: 'SQL',
|
||||||
|
parallel: 'safe',
|
||||||
|
strict: true,
|
||||||
|
behavior: 'immutable',
|
||||||
|
body: `SELECT public.cube(public.cube(public.cube(public.earth()*cos(radians(latitude))*cos(radians(longitude))),public.earth()*cos(radians(latitude))*sin(radians(longitude))),public.earth()*sin(radians(latitude)))::public.earth`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const users_delete_audit = registerFunction({
|
||||||
|
name: 'users_delete_audit',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO users_audit ("userId")
|
||||||
|
SELECT "id"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const partners_delete_audit = registerFunction({
|
||||||
|
name: 'partners_delete_audit',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO partners_audit ("sharedById", "sharedWithId")
|
||||||
|
SELECT "sharedById", "sharedWithId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const assets_delete_audit = registerFunction({
|
||||||
|
name: 'assets_delete_audit',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO assets_audit ("assetId", "ownerId")
|
||||||
|
SELECT "id", "ownerId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
109
server/src/schema/index.ts
Normal file
109
server/src/schema/index.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { asset_face_source_type, assets_status_enum } from 'src/schema/enums';
|
||||||
|
import {
|
||||||
|
assets_delete_audit,
|
||||||
|
f_concat_ws,
|
||||||
|
f_unaccent,
|
||||||
|
immich_uuid_v7,
|
||||||
|
ll_to_earth_public,
|
||||||
|
partners_delete_audit,
|
||||||
|
updated_at,
|
||||||
|
users_delete_audit,
|
||||||
|
} from 'src/schema/functions';
|
||||||
|
import { ActivityTable } from 'src/schema/tables/activity.table';
|
||||||
|
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
|
||||||
|
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
||||||
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
|
import { APIKeyTable } from 'src/schema/tables/api-key.table';
|
||||||
|
import { AssetAuditTable } from 'src/schema/tables/asset-audit.table';
|
||||||
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
|
import { AssetFileTable } from 'src/schema/tables/asset-files.table';
|
||||||
|
import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table';
|
||||||
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
|
import { AuditTable } from 'src/schema/tables/audit.table';
|
||||||
|
import { ExifTable } from 'src/schema/tables/exif.table';
|
||||||
|
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||||
|
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||||
|
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||||
|
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||||
|
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
||||||
|
import { MoveTable } from 'src/schema/tables/move.table';
|
||||||
|
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
||||||
|
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
||||||
|
import { PartnerTable } from 'src/schema/tables/partner.table';
|
||||||
|
import { PersonTable } from 'src/schema/tables/person.table';
|
||||||
|
import { SessionTable } from 'src/schema/tables/session.table';
|
||||||
|
import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table';
|
||||||
|
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||||
|
import { SmartSearchTable } from 'src/schema/tables/smart-search.table';
|
||||||
|
import { StackTable } from 'src/schema/tables/stack.table';
|
||||||
|
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
|
||||||
|
import { SystemMetadataTable } from 'src/schema/tables/system-metadata.table';
|
||||||
|
import { TagAssetTable } from 'src/schema/tables/tag-asset.table';
|
||||||
|
import { TagClosureTable } from 'src/schema/tables/tag-closure.table';
|
||||||
|
import { UserAuditTable } from 'src/schema/tables/user-audit.table';
|
||||||
|
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
|
||||||
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
|
||||||
|
import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'vectors', 'plpgsql'])
|
||||||
|
@ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' })
|
||||||
|
@ConfigurationParameter({
|
||||||
|
name: 'vectors.pgvector_compatibility',
|
||||||
|
value: () => 'on',
|
||||||
|
scope: 'user',
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
|
@Database({ name: 'immich' })
|
||||||
|
export class ImmichDatabase {
|
||||||
|
tables = [
|
||||||
|
ActivityTable,
|
||||||
|
AlbumAssetTable,
|
||||||
|
AlbumUserTable,
|
||||||
|
AlbumTable,
|
||||||
|
APIKeyTable,
|
||||||
|
AssetAuditTable,
|
||||||
|
AssetFaceTable,
|
||||||
|
AssetJobStatusTable,
|
||||||
|
AssetTable,
|
||||||
|
AssetFileTable,
|
||||||
|
AuditTable,
|
||||||
|
ExifTable,
|
||||||
|
FaceSearchTable,
|
||||||
|
GeodataPlacesTable,
|
||||||
|
LibraryTable,
|
||||||
|
MemoryAssetTable,
|
||||||
|
MemoryTable,
|
||||||
|
MoveTable,
|
||||||
|
NaturalEarthCountriesTable,
|
||||||
|
PartnerAuditTable,
|
||||||
|
PartnerTable,
|
||||||
|
PersonTable,
|
||||||
|
SessionTable,
|
||||||
|
SharedLinkAssetTable,
|
||||||
|
SharedLinkTable,
|
||||||
|
SmartSearchTable,
|
||||||
|
StackTable,
|
||||||
|
SessionSyncCheckpointTable,
|
||||||
|
SystemMetadataTable,
|
||||||
|
TagAssetTable,
|
||||||
|
TagClosureTable,
|
||||||
|
UserAuditTable,
|
||||||
|
UserMetadataTable,
|
||||||
|
UserTable,
|
||||||
|
VersionHistoryTable,
|
||||||
|
];
|
||||||
|
|
||||||
|
functions = [
|
||||||
|
immich_uuid_v7,
|
||||||
|
updated_at,
|
||||||
|
f_concat_ws,
|
||||||
|
f_unaccent,
|
||||||
|
ll_to_earth_public,
|
||||||
|
users_delete_audit,
|
||||||
|
partners_delete_audit,
|
||||||
|
assets_delete_audit,
|
||||||
|
];
|
||||||
|
|
||||||
|
enum = [assets_status_enum, asset_face_source_type];
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
@ -11,10 +12,10 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('activity')
|
@Table('activity')
|
||||||
|
@UpdatedAtTrigger('activity_updated_at')
|
||||||
@Index({
|
@Index({
|
||||||
name: 'IDX_activity_like',
|
name: 'IDX_activity_like',
|
||||||
columns: ['assetId', 'userId', 'albumId'],
|
columns: ['assetId', 'userId', 'albumId'],
|
||||||
@ -35,9 +36,14 @@ export class ActivityTable {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_activity_update_id')
|
@ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
@UpdateIdColumn()
|
albumId!: string;
|
||||||
updateId!: string;
|
|
||||||
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||||
|
assetId!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'text', default: null })
|
@Column({ type: 'text', default: null })
|
||||||
comment!: string | null;
|
comment!: string | null;
|
||||||
@ -45,12 +51,7 @@ export class ActivityTable {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isLiked!: boolean;
|
isLiked!: boolean;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
@ColumnIndex('IDX_activity_update_id')
|
||||||
assetId!: string | null;
|
@UpdateIdColumn()
|
||||||
|
updateId!: string;
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
|
||||||
userId!: string;
|
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
|
||||||
albumId!: string;
|
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,6 @@ import { ColumnIndex, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-
|
|||||||
|
|
||||||
@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
|
@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
|
||||||
export class AlbumAssetTable {
|
export class AlbumAssetTable {
|
||||||
@ForeignKeyColumn(() => AssetTable, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
onUpdate: 'CASCADE',
|
|
||||||
nullable: false,
|
|
||||||
primary: true,
|
|
||||||
})
|
|
||||||
@ColumnIndex()
|
|
||||||
assetsId!: string;
|
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AlbumTable, {
|
@ForeignKeyColumn(() => AlbumTable, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
onUpdate: 'CASCADE',
|
onUpdate: 'CASCADE',
|
||||||
@ -22,6 +13,15 @@ export class AlbumAssetTable {
|
|||||||
@ColumnIndex()
|
@ColumnIndex()
|
||||||
albumsId!: string;
|
albumsId!: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => AssetTable, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
nullable: false,
|
||||||
|
primary: true,
|
||||||
|
})
|
||||||
|
@ColumnIndex()
|
||||||
|
assetsId!: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AssetOrder } from 'src/enum';
|
import { AssetOrder } from 'src/enum';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
@ -10,10 +11,10 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' })
|
@Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' })
|
||||||
|
@UpdatedAtTrigger('albums_updated_at')
|
||||||
export class AlbumTable {
|
export class AlbumTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
@ -24,28 +25,33 @@ export class AlbumTable {
|
|||||||
@Column({ default: 'Untitled Album' })
|
@Column({ default: 'Untitled Album' })
|
||||||
albumName!: string;
|
albumName!: string;
|
||||||
|
|
||||||
@Column({ type: 'text', default: '' })
|
|
||||||
description!: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => AssetTable, {
|
||||||
|
nullable: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
comment: 'Asset ID to be used as thumbnail',
|
||||||
|
})
|
||||||
|
albumThumbnailAssetId!: string;
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_albums_update_id')
|
@Column({ type: 'text', default: '' })
|
||||||
@UpdateIdColumn()
|
description!: string;
|
||||||
updateId?: string;
|
|
||||||
|
|
||||||
@DeleteDateColumn()
|
@DeleteDateColumn()
|
||||||
deletedAt!: Date | null;
|
deletedAt!: Date | null;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AssetTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
|
||||||
albumThumbnailAssetId!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
isActivityEnabled!: boolean;
|
isActivityEnabled!: boolean;
|
||||||
|
|
||||||
@Column({ default: AssetOrder.DESC })
|
@Column({ default: AssetOrder.DESC })
|
||||||
order!: AssetOrder;
|
order!: AssetOrder;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_albums_update_id')
|
||||||
|
@UpdateIdColumn()
|
||||||
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
@ -8,22 +9,19 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('api_keys')
|
@Table('api_keys')
|
||||||
|
@UpdatedAtTrigger('api_keys_updated_at')
|
||||||
export class APIKeyTable {
|
export class APIKeyTable {
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: string;
|
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
key!: string;
|
key!: string;
|
||||||
|
|
||||||
@Column({ array: true, type: 'character varying' })
|
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||||
permissions!: Permission[];
|
userId!: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
@ -31,10 +29,13 @@ export class APIKeyTable {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ array: true, type: 'character varying' })
|
||||||
|
permissions!: Permission[];
|
||||||
|
|
||||||
@ColumnIndex({ name: 'IDX_api_keys_update_id' })
|
@ColumnIndex({ name: 'IDX_api_keys_update_id' })
|
||||||
@UpdateIdColumn()
|
@UpdateIdColumn()
|
||||||
updateId?: string;
|
updateId?: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
|
||||||
userId!: string;
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Column, ColumnIndex, CreateDateColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('assets_audit')
|
@Table('assets_audit')
|
||||||
export class AssetAuditTable {
|
export class AssetAuditTable {
|
||||||
@PrimaryGeneratedColumn({ type: 'v7' })
|
@PrimaryGeneratedUuidV7Column()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_assets_audit_asset_id')
|
@ColumnIndex('IDX_assets_audit_asset_id')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SourceType } from 'src/enum';
|
import { SourceType } from 'src/enum';
|
||||||
|
import { asset_face_source_type } from 'src/schema/enums';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { PersonTable } from 'src/schema/tables/person.table';
|
import { PersonTable } from 'src/schema/tables/person.table';
|
||||||
import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
|
import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
|
||||||
@ -7,8 +8,11 @@ import { Column, DeleteDateColumn, ForeignKeyColumn, Index, PrimaryGeneratedColu
|
|||||||
@Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] })
|
@Index({ name: 'IDX_asset_faces_assetId_personId', columns: ['assetId', 'personId'] })
|
||||||
@Index({ columns: ['personId', 'assetId'] })
|
@Index({ columns: ['personId', 'assetId'] })
|
||||||
export class AssetFaceTable {
|
export class AssetFaceTable {
|
||||||
@PrimaryGeneratedColumn()
|
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
id!: string;
|
assetId!: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => PersonTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true })
|
||||||
|
personId!: string | null;
|
||||||
|
|
||||||
@Column({ default: 0, type: 'integer' })
|
@Column({ default: 0, type: 'integer' })
|
||||||
imageWidth!: number;
|
imageWidth!: number;
|
||||||
@ -28,15 +32,12 @@ export class AssetFaceTable {
|
|||||||
@Column({ default: 0, type: 'integer' })
|
@Column({ default: 0, type: 'integer' })
|
||||||
boundingBoxY2!: number;
|
boundingBoxY2!: number;
|
||||||
|
|
||||||
@Column({ default: SourceType.MACHINE_LEARNING, enumName: 'sourcetype', enum: SourceType })
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ default: SourceType.MACHINE_LEARNING, enum: asset_face_source_type })
|
||||||
sourceType!: SourceType;
|
sourceType!: SourceType;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
|
||||||
assetId!: string;
|
|
||||||
|
|
||||||
@ForeignKeyColumn(() => PersonTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true })
|
|
||||||
personId!: string | null;
|
|
||||||
|
|
||||||
@DeleteDateColumn()
|
@DeleteDateColumn()
|
||||||
deletedAt!: Date | null;
|
deletedAt!: Date | null;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AssetFileType } from 'src/enum';
|
import { AssetFileType } from 'src/enum';
|
||||||
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
ColumnIndex,
|
||||||
@ -9,18 +10,18 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Unique,
|
Unique,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Unique({ name: 'UQ_assetId_type', columns: ['assetId', 'type'] })
|
|
||||||
@Table('asset_files')
|
@Table('asset_files')
|
||||||
|
@Unique({ name: 'UQ_assetId_type', columns: ['assetId', 'type'] })
|
||||||
|
@UpdatedAtTrigger('asset_files_updated_at')
|
||||||
export class AssetFileTable {
|
export class AssetFileTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_asset_files_assetId')
|
@ColumnIndex('IDX_asset_files_assetId')
|
||||||
@ForeignKeyColumn(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
assetId?: AssetEntity;
|
assetId?: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
@ -28,13 +29,13 @@ export class AssetFileTable {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_asset_files_update_id')
|
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
type!: AssetFileType;
|
type!: AssetFileType;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
path!: string;
|
path!: string;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_asset_files_update_id')
|
||||||
|
@UpdateIdColumn()
|
||||||
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/entities/asset.entity';
|
||||||
import { AssetStatus, AssetType } from 'src/enum';
|
import { AssetStatus, AssetType } from 'src/enum';
|
||||||
|
import { assets_status_enum } from 'src/schema/enums';
|
||||||
|
import { assets_delete_audit } from 'src/schema/functions';
|
||||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||||
import { StackTable } from 'src/schema/tables/stack.table';
|
import { StackTable } from 'src/schema/tables/stack.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
|
AfterDeleteTrigger,
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
ColumnIndex,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
@ -13,10 +17,17 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('assets')
|
@Table('assets')
|
||||||
|
@UpdatedAtTrigger('assets_updated_at')
|
||||||
|
@AfterDeleteTrigger({
|
||||||
|
name: 'assets_delete_audit',
|
||||||
|
scope: 'statement',
|
||||||
|
function: assets_delete_audit,
|
||||||
|
referencingOldTableAs: 'old',
|
||||||
|
when: 'pg_trigger_depth() = 0',
|
||||||
|
})
|
||||||
// Checksums must be unique per user and library
|
// Checksums must be unique per user and library
|
||||||
@Index({
|
@Index({
|
||||||
name: ASSET_CHECKSUM_CONSTRAINT,
|
name: ASSET_CHECKSUM_CONSTRAINT,
|
||||||
@ -30,7 +41,11 @@ import {
|
|||||||
unique: true,
|
unique: true,
|
||||||
where: '("libraryId" IS NOT NULL)',
|
where: '("libraryId" IS NOT NULL)',
|
||||||
})
|
})
|
||||||
@Index({ name: 'idx_local_date_time', expression: `(("localDateTime" AT TIME ZONE 'UTC'::text))::date` })
|
@Index({
|
||||||
|
name: 'idx_local_date_time',
|
||||||
|
expression: `(("localDateTime" at time zone 'UTC')::date)`,
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
@Index({
|
@Index({
|
||||||
name: 'idx_local_date_time_month',
|
name: 'idx_local_date_time_month',
|
||||||
expression: `(date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text)`,
|
expression: `(date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text)`,
|
||||||
@ -38,9 +53,10 @@ import {
|
|||||||
@Index({ name: 'IDX_originalPath_libraryId', columns: ['originalPath', 'libraryId'] })
|
@Index({ name: 'IDX_originalPath_libraryId', columns: ['originalPath', 'libraryId'] })
|
||||||
@Index({ name: 'IDX_asset_id_stackId', columns: ['id', 'stackId'] })
|
@Index({ name: 'IDX_asset_id_stackId', columns: ['id', 'stackId'] })
|
||||||
@Index({
|
@Index({
|
||||||
name: 'idx_originalFileName_trigram',
|
name: 'idx_originalfilename_trigram',
|
||||||
using: 'gin',
|
using: 'gin',
|
||||||
expression: 'f_unaccent(("originalFileName")::text)',
|
expression: 'f_unaccent("originalFileName") gin_trgm_ops',
|
||||||
|
synchronize: false,
|
||||||
})
|
})
|
||||||
// For all assets, each originalpath must be unique per user and library
|
// For all assets, each originalpath must be unique per user and library
|
||||||
export class AssetTable {
|
export class AssetTable {
|
||||||
@ -53,75 +69,50 @@ export class AssetTable {
|
|||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => LibraryTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
|
||||||
libraryId?: string | null;
|
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
deviceId!: string;
|
deviceId!: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
type!: AssetType;
|
type!: AssetType;
|
||||||
|
|
||||||
@Column({ type: 'enum', enum: AssetStatus, default: AssetStatus.ACTIVE })
|
|
||||||
status!: AssetStatus;
|
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
originalPath!: string;
|
originalPath!: string;
|
||||||
|
|
||||||
@Column({ type: 'bytea', nullable: true })
|
|
||||||
thumbhash!: Buffer | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true, default: '' })
|
|
||||||
encodedVideoPath!: string | null;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt!: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt!: Date;
|
|
||||||
|
|
||||||
@ColumnIndex('IDX_assets_update_id')
|
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
|
||||||
|
|
||||||
@DeleteDateColumn()
|
|
||||||
deletedAt!: Date | null;
|
|
||||||
|
|
||||||
@ColumnIndex('idx_asset_file_created_at')
|
@ColumnIndex('idx_asset_file_created_at')
|
||||||
@Column({ type: 'timestamp with time zone', default: null })
|
@Column({ type: 'timestamp with time zone', default: null })
|
||||||
fileCreatedAt!: Date;
|
fileCreatedAt!: Date;
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', default: null })
|
|
||||||
localDateTime!: Date;
|
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', default: null })
|
@Column({ type: 'timestamp with time zone', default: null })
|
||||||
fileModifiedAt!: Date;
|
fileModifiedAt!: Date;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
isArchived!: boolean;
|
duration!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'character varying', nullable: true, default: '' })
|
||||||
isExternal!: boolean;
|
encodedVideoPath!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
|
||||||
isOffline!: boolean;
|
|
||||||
|
|
||||||
@Column({ type: 'bytea' })
|
@Column({ type: 'bytea' })
|
||||||
@ColumnIndex()
|
@ColumnIndex()
|
||||||
checksum!: Buffer; // sha1 checksum
|
checksum!: Buffer; // sha1 checksum
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
duration!: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
isVisible!: boolean;
|
isVisible!: boolean;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
@ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
||||||
livePhotoVideoId!: string | null;
|
livePhotoVideoId!: string | null;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isArchived!: boolean;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
@ColumnIndex()
|
@ColumnIndex()
|
||||||
originalFileName!: string;
|
originalFileName!: string;
|
||||||
@ -129,10 +120,35 @@ export class AssetTable {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
sidecarPath!: string | null;
|
sidecarPath!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'bytea', nullable: true })
|
||||||
|
thumbhash!: Buffer | null;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isOffline!: boolean;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => LibraryTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||||
|
libraryId?: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isExternal!: boolean;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt!: Date | null;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', default: null })
|
||||||
|
localDateTime!: Date;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => StackTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
@ForeignKeyColumn(() => StackTable, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
||||||
stackId?: string | null;
|
stackId?: string | null;
|
||||||
|
|
||||||
@ColumnIndex('IDX_assets_duplicateId')
|
@ColumnIndex('IDX_assets_duplicateId')
|
||||||
@Column({ type: 'uuid', nullable: true })
|
@Column({ type: 'uuid', nullable: true })
|
||||||
duplicateId!: string | null;
|
duplicateId!: string | null;
|
||||||
|
|
||||||
|
@Column({ enum: assets_status_enum, default: AssetStatus.ACTIVE })
|
||||||
|
status!: AssetStatus;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_assets_update_id')
|
||||||
|
@UpdateIdColumn()
|
||||||
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { Column, CreateDateColumn, Index, PrimaryColumn, Table } from 'src/sql-t
|
|||||||
@Table('audit')
|
@Table('audit')
|
||||||
@Index({ name: 'IDX_ownerId_createdAt', columns: ['ownerId', 'createdAt'] })
|
@Index({ name: 'IDX_ownerId_createdAt', columns: ['ownerId', 'createdAt'] })
|
||||||
export class AuditTable {
|
export class AuditTable {
|
||||||
@PrimaryColumn({ type: 'integer', default: 'increment', synchronize: false })
|
@PrimaryColumn({ type: 'serial', synchronize: false })
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn, UpdateIdColumn } from 'src/sql-tools';
|
import { Column, ColumnIndex, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('exif')
|
@Table('exif')
|
||||||
|
@UpdatedAtTrigger('asset_exif_updated_at')
|
||||||
export class ExifTable {
|
export class ExifTable {
|
||||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', primary: true })
|
||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
@UpdateDateColumn({ default: () => 'clock_timestamp()' })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
updatedAt?: Date;
|
make!: string | null;
|
||||||
|
|
||||||
@ColumnIndex('IDX_asset_exif_update_id')
|
@Column({ type: 'character varying', nullable: true })
|
||||||
@UpdateIdColumn()
|
model!: string | null;
|
||||||
updateId?: string;
|
|
||||||
|
|
||||||
/* General info */
|
|
||||||
@Column({ type: 'text', default: '' })
|
|
||||||
description!: string; // or caption
|
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ type: 'integer', nullable: true })
|
||||||
exifImageWidth!: number | null;
|
exifImageWidth!: number | null;
|
||||||
@ -35,43 +32,6 @@ export class ExifTable {
|
|||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
modifyDate!: Date | null;
|
modifyDate!: Date | null;
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
timeZone!: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'double precision', nullable: true })
|
|
||||||
latitude!: number | null;
|
|
||||||
|
|
||||||
@Column({ type: 'double precision', nullable: true })
|
|
||||||
longitude!: number | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
projectionType!: string | null;
|
|
||||||
|
|
||||||
@ColumnIndex('exif_city')
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
city!: string | null;
|
|
||||||
|
|
||||||
@ColumnIndex('IDX_live_photo_cid')
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
livePhotoCID!: string | null;
|
|
||||||
|
|
||||||
@ColumnIndex('IDX_auto_stack_id')
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
autoStackId!: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
state!: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
country!: string | null;
|
|
||||||
|
|
||||||
/* Image info */
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
make!: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
model!: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
lensModel!: string | null;
|
lensModel!: string | null;
|
||||||
|
|
||||||
@ -84,9 +44,41 @@ export class ExifTable {
|
|||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ type: 'integer', nullable: true })
|
||||||
iso!: number | null;
|
iso!: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'double precision', nullable: true })
|
||||||
|
latitude!: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'double precision', nullable: true })
|
||||||
|
longitude!: number | null;
|
||||||
|
|
||||||
|
@ColumnIndex('exif_city')
|
||||||
|
@Column({ type: 'character varying', nullable: true })
|
||||||
|
city!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'character varying', nullable: true })
|
||||||
|
state!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'character varying', nullable: true })
|
||||||
|
country!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'text', default: '' })
|
||||||
|
description!: string; // or caption
|
||||||
|
|
||||||
|
@Column({ type: 'double precision', nullable: true })
|
||||||
|
fps?: number | null;
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
exposureTime!: string | null;
|
exposureTime!: string | null;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_live_photo_cid')
|
||||||
|
@Column({ type: 'character varying', nullable: true })
|
||||||
|
livePhotoCID!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'character varying', nullable: true })
|
||||||
|
timeZone!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'character varying', nullable: true })
|
||||||
|
projectionType!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
profileDescription!: string | null;
|
profileDescription!: string | null;
|
||||||
|
|
||||||
@ -96,10 +88,17 @@ export class ExifTable {
|
|||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ type: 'integer', nullable: true })
|
||||||
bitsPerSample!: number | null;
|
bitsPerSample!: number | null;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_auto_stack_id')
|
||||||
|
@Column({ type: 'character varying', nullable: true })
|
||||||
|
autoStackId!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ type: 'integer', nullable: true })
|
||||||
rating!: number | null;
|
rating!: number | null;
|
||||||
|
|
||||||
/* Video info */
|
@UpdateDateColumn({ default: () => 'clock_timestamp()' })
|
||||||
@Column({ type: 'double precision', nullable: true })
|
updatedAt?: Date;
|
||||||
fps?: number | null;
|
|
||||||
|
@ColumnIndex('IDX_asset_exif_update_id')
|
||||||
|
@UpdateIdColumn()
|
||||||
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
|
import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'face_search', primaryConstraintName: 'face_search_pkey' })
|
@Table({ name: 'face_search', primaryConstraintName: 'face_search_pkey' })
|
||||||
|
@Index({
|
||||||
|
name: 'face_index',
|
||||||
|
using: 'hnsw',
|
||||||
|
expression: `embedding vector_cosine_ops`,
|
||||||
|
with: 'ef_construction = 300, m = 16',
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
export class FaceSearchTable {
|
export class FaceSearchTable {
|
||||||
@ForeignKeyColumn(() => AssetFaceTable, {
|
@ForeignKeyColumn(() => AssetFaceTable, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
@ -10,7 +17,6 @@ export class FaceSearchTable {
|
|||||||
})
|
})
|
||||||
faceId!: string;
|
faceId!: string;
|
||||||
|
|
||||||
@ColumnIndex({ name: 'face_index', synchronize: false })
|
@Column({ type: 'vector', length: 512, synchronize: false })
|
||||||
@Column({ type: 'vector', array: true, length: 512, synchronize: false })
|
|
||||||
embedding!: string;
|
embedding!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,35 @@
|
|||||||
import { Column, Index, PrimaryColumn, Table } from 'src/sql-tools';
|
import { Column, Index, PrimaryColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Index({ name: 'idx_geodata_places_alternate_names', expression: 'f_unaccent("alternateNames") gin_trgm_ops' })
|
@Table({ name: 'geodata_places' })
|
||||||
@Index({ name: 'idx_geodata_places_admin1_name', expression: 'f_unaccent("admin1Name") gin_trgm_ops' })
|
@Index({
|
||||||
@Index({ name: 'idx_geodata_places_admin2_name', expression: 'f_unaccent("admin2Name") gin_trgm_ops' })
|
name: 'idx_geodata_places_alternate_names',
|
||||||
@Index({ name: 'idx_geodata_places_name', expression: 'f_unaccent("name") gin_trgm_ops' })
|
using: 'gin',
|
||||||
@Index({ name: 'idx_geodata_places_gist_earthcoord', expression: 'll_to_earth_public(latitude, longitude)' })
|
expression: 'f_unaccent("alternateNames") gin_trgm_ops',
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
|
@Index({
|
||||||
|
name: 'idx_geodata_places_admin1_name',
|
||||||
|
using: 'gin',
|
||||||
|
expression: 'f_unaccent("admin1Name") gin_trgm_ops',
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
|
@Index({
|
||||||
|
name: 'idx_geodata_places_admin2_name',
|
||||||
|
using: 'gin',
|
||||||
|
expression: 'f_unaccent("admin2Name") gin_trgm_ops',
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
|
@Index({
|
||||||
|
name: 'idx_geodata_places_name',
|
||||||
|
using: 'gin',
|
||||||
|
expression: 'f_unaccent("name") gin_trgm_ops',
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
|
@Index({
|
||||||
|
name: 'idx_geodata_places_gist_earthcoord',
|
||||||
|
expression: 'll_to_earth_public(latitude, longitude)',
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
@Table({ name: 'idx_geodata_places', synchronize: false })
|
@Table({ name: 'idx_geodata_places', synchronize: false })
|
||||||
export class GeodataPlacesTable {
|
export class GeodataPlacesTable {
|
||||||
@PrimaryColumn({ type: 'integer' })
|
@PrimaryColumn({ type: 'integer' })
|
||||||
@ -28,41 +53,8 @@ export class GeodataPlacesTable {
|
|||||||
@Column({ type: 'character varying', length: 80, nullable: true })
|
@Column({ type: 'character varying', length: 80, nullable: true })
|
||||||
admin2Code!: string;
|
admin2Code!: string;
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
admin1Name!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
admin2Name!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
alternateNames!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'date' })
|
@Column({ type: 'date' })
|
||||||
modificationDate!: Date;
|
modificationDate!: Date;
|
||||||
}
|
|
||||||
|
|
||||||
@Table({ name: 'geodata_places_tmp', synchronize: false })
|
|
||||||
export class GeodataPlacesTempEntity {
|
|
||||||
@PrimaryColumn({ type: 'integer' })
|
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', length: 200 })
|
|
||||||
name!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'double precision' })
|
|
||||||
longitude!: number;
|
|
||||||
|
|
||||||
@Column({ type: 'double precision' })
|
|
||||||
latitude!: number;
|
|
||||||
|
|
||||||
@Column({ type: 'character', length: 2 })
|
|
||||||
countryCode!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', length: 20, nullable: true })
|
|
||||||
admin1Code!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', length: 80, nullable: true })
|
|
||||||
admin2Code!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
admin1Name!: string;
|
admin1Name!: string;
|
||||||
@ -72,7 +64,4 @@ export class GeodataPlacesTempEntity {
|
|||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
alternateNames!: string;
|
alternateNames!: string;
|
||||||
|
|
||||||
@Column({ type: 'date' })
|
|
||||||
modificationDate!: Date;
|
|
||||||
}
|
}
|
||||||
|
@ -1,73 +1,35 @@
|
|||||||
import { ActivityTable } from 'src/schema/tables/activity.table';
|
import 'src/schema/tables/activity.table';
|
||||||
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
|
import 'src/schema/tables/album-asset.table';
|
||||||
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
import 'src/schema/tables/album-user.table';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import 'src/schema/tables/album.table';
|
||||||
import { APIKeyTable } from 'src/schema/tables/api-key.table';
|
import 'src/schema/tables/api-key.table';
|
||||||
import { AssetAuditTable } from 'src/schema/tables/asset-audit.table';
|
import 'src/schema/tables/asset-audit.table';
|
||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import 'src/schema/tables/asset-face.table';
|
||||||
import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table';
|
import 'src/schema/tables/asset-files.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import 'src/schema/tables/asset-job-status.table';
|
||||||
import { AuditTable } from 'src/schema/tables/audit.table';
|
import 'src/schema/tables/asset.table';
|
||||||
import { ExifTable } from 'src/schema/tables/exif.table';
|
import 'src/schema/tables/audit.table';
|
||||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
import 'src/schema/tables/exif.table';
|
||||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
import 'src/schema/tables/face-search.table';
|
||||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
import 'src/schema/tables/geodata-places.table';
|
||||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
import 'src/schema/tables/library.table';
|
||||||
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
import 'src/schema/tables/memory.table';
|
||||||
import { MoveTable } from 'src/schema/tables/move.table';
|
import 'src/schema/tables/memory_asset.table';
|
||||||
import {
|
import 'src/schema/tables/move.table';
|
||||||
NaturalEarthCountriesTable,
|
import 'src/schema/tables/natural-earth-countries.table';
|
||||||
NaturalEarthCountriesTempTable,
|
import 'src/schema/tables/partner-audit.table';
|
||||||
} from 'src/schema/tables/natural-earth-countries.table';
|
import 'src/schema/tables/partner.table';
|
||||||
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
import 'src/schema/tables/person.table';
|
||||||
import { PartnerTable } from 'src/schema/tables/partner.table';
|
import 'src/schema/tables/session.table';
|
||||||
import { PersonTable } from 'src/schema/tables/person.table';
|
import 'src/schema/tables/shared-link-asset.table';
|
||||||
import { SessionTable } from 'src/schema/tables/session.table';
|
import 'src/schema/tables/shared-link.table';
|
||||||
import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table';
|
import 'src/schema/tables/smart-search.table';
|
||||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
import 'src/schema/tables/stack.table';
|
||||||
import { SmartSearchTable } from 'src/schema/tables/smart-search.table';
|
import 'src/schema/tables/sync-checkpoint.table';
|
||||||
import { StackTable } from 'src/schema/tables/stack.table';
|
import 'src/schema/tables/system-metadata.table';
|
||||||
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
|
import 'src/schema/tables/tag-asset.table';
|
||||||
import { SystemMetadataTable } from 'src/schema/tables/system-metadata.table';
|
import 'src/schema/tables/tag-closure.table';
|
||||||
import { TagAssetTable } from 'src/schema/tables/tag-asset.table';
|
import 'src/schema/tables/user-audit.table';
|
||||||
import { UserAuditTable } from 'src/schema/tables/user-audit.table';
|
import 'src/schema/tables/user-metadata.table';
|
||||||
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
|
import 'src/schema/tables/user.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import 'src/schema/tables/version-history.table';
|
||||||
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
|
|
||||||
|
|
||||||
export const tables = [
|
|
||||||
ActivityTable,
|
|
||||||
AlbumAssetTable,
|
|
||||||
AlbumUserTable,
|
|
||||||
AlbumTable,
|
|
||||||
APIKeyTable,
|
|
||||||
AssetAuditTable,
|
|
||||||
AssetFaceTable,
|
|
||||||
AssetJobStatusTable,
|
|
||||||
AssetTable,
|
|
||||||
AuditTable,
|
|
||||||
ExifTable,
|
|
||||||
FaceSearchTable,
|
|
||||||
GeodataPlacesTable,
|
|
||||||
LibraryTable,
|
|
||||||
MemoryAssetTable,
|
|
||||||
MemoryTable,
|
|
||||||
MoveTable,
|
|
||||||
NaturalEarthCountriesTable,
|
|
||||||
NaturalEarthCountriesTempTable,
|
|
||||||
PartnerAuditTable,
|
|
||||||
PartnerTable,
|
|
||||||
PersonTable,
|
|
||||||
SessionTable,
|
|
||||||
SharedLinkAssetTable,
|
|
||||||
SharedLinkTable,
|
|
||||||
SmartSearchTable,
|
|
||||||
StackTable,
|
|
||||||
SessionSyncCheckpointTable,
|
|
||||||
SystemMetadataTable,
|
|
||||||
TagAssetTable,
|
|
||||||
UserAuditTable,
|
|
||||||
UserMetadataTable,
|
|
||||||
UserTable,
|
|
||||||
VersionHistoryTable,
|
|
||||||
];
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
@ -8,10 +9,10 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('libraries')
|
@Table('libraries')
|
||||||
|
@UpdatedAtTrigger('libraries_updated_at')
|
||||||
export class LibraryTable {
|
export class LibraryTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
@ -34,13 +35,13 @@ export class LibraryTable {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_libraries_update_id')
|
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
|
||||||
|
|
||||||
@DeleteDateColumn()
|
@DeleteDateColumn()
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
refreshedAt!: Date | null;
|
refreshedAt!: Date | null;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_libraries_update_id')
|
||||||
|
@UpdateIdColumn()
|
||||||
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { MemoryType } from 'src/enum';
|
import { MemoryType } from 'src/enum';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
@ -9,11 +10,11 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
import { MemoryData } from 'src/types';
|
import { MemoryData } from 'src/types';
|
||||||
|
|
||||||
@Table('memories')
|
@Table('memories')
|
||||||
|
@UpdatedAtTrigger('memories_updated_at')
|
||||||
export class MemoryTable<T extends MemoryType = MemoryType> {
|
export class MemoryTable<T extends MemoryType = MemoryType> {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
@ -24,10 +25,6 @@ export class MemoryTable<T extends MemoryType = MemoryType> {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_memories_update_id')
|
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId?: string;
|
|
||||||
|
|
||||||
@DeleteDateColumn()
|
@DeleteDateColumn()
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
|
||||||
@ -48,13 +45,17 @@ export class MemoryTable<T extends MemoryType = MemoryType> {
|
|||||||
@Column({ type: 'timestamp with time zone' })
|
@Column({ type: 'timestamp with time zone' })
|
||||||
memoryAt!: Date;
|
memoryAt!: Date;
|
||||||
|
|
||||||
|
/** when the user last viewed the memory */
|
||||||
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
|
seenAt?: Date;
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
showAt?: Date;
|
showAt?: Date;
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
hideAt?: Date;
|
hideAt?: Date;
|
||||||
|
|
||||||
/** when the user last viewed the memory */
|
@ColumnIndex('IDX_memories_update_id')
|
||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@UpdateIdColumn()
|
||||||
seenAt?: Date;
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@ import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
|
|||||||
|
|
||||||
@Table('memories_assets_assets')
|
@Table('memories_assets_assets')
|
||||||
export class MemoryAssetTable {
|
export class MemoryAssetTable {
|
||||||
@ColumnIndex()
|
|
||||||
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
|
||||||
assetsId!: string;
|
|
||||||
|
|
||||||
@ColumnIndex()
|
@ColumnIndex()
|
||||||
@ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||||
memoriesId!: string;
|
memoriesId!: string;
|
||||||
|
|
||||||
|
@ColumnIndex()
|
||||||
|
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
|
||||||
|
assetsId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,8 @@
|
|||||||
import { Column, PrimaryColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
|
import { Column, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'naturalearth_countries', synchronize: false })
|
@Table({ name: 'naturalearth_countries' })
|
||||||
export class NaturalEarthCountriesTable {
|
export class NaturalEarthCountriesTable {
|
||||||
@PrimaryColumn({ type: 'serial' })
|
@PrimaryGeneratedColumn({ strategy: 'identity' })
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', length: 50 })
|
|
||||||
admin!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', length: 3 })
|
|
||||||
admin_a3!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', length: 50 })
|
|
||||||
type!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'polygon' })
|
|
||||||
coordinates!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table({ name: 'naturalearth_countries_tmp', synchronize: false })
|
|
||||||
export class NaturalEarthCountriesTempTable {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ type: 'character varying', length: 50 })
|
@Column({ type: 'character varying', length: 50 })
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Column, ColumnIndex, CreateDateColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('partners_audit')
|
@Table('partners_audit')
|
||||||
export class PartnerAuditTable {
|
export class PartnerAuditTable {
|
||||||
@PrimaryGeneratedColumn({ type: 'v7' })
|
@PrimaryGeneratedUuidV7Column()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_partners_audit_shared_by_id')
|
@ColumnIndex('IDX_partners_audit_shared_by_id')
|
||||||
|
@ -1,15 +1,25 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
|
import { partners_delete_audit } from 'src/schema/functions';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
|
AfterDeleteTrigger,
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
ColumnIndex,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
ForeignKeyColumn,
|
ForeignKeyColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('partners')
|
@Table('partners')
|
||||||
|
@UpdatedAtTrigger('partners_updated_at')
|
||||||
|
@AfterDeleteTrigger({
|
||||||
|
name: 'partners_delete_audit',
|
||||||
|
scope: 'statement',
|
||||||
|
function: partners_delete_audit,
|
||||||
|
referencingOldTableAs: 'old',
|
||||||
|
when: 'pg_trigger_depth() = 0',
|
||||||
|
})
|
||||||
export class PartnerTable {
|
export class PartnerTable {
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', primary: true })
|
||||||
sharedById!: string;
|
sharedById!: string;
|
||||||
@ -23,10 +33,10 @@ export class PartnerTable {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
inTimeline!: boolean;
|
||||||
|
|
||||||
@ColumnIndex('IDX_partners_update_id')
|
@ColumnIndex('IDX_partners_update_id')
|
||||||
@UpdateIdColumn()
|
@UpdateIdColumn()
|
||||||
updateId!: string;
|
updateId!: string;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
|
||||||
inTimeline!: boolean;
|
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
@ -9,10 +10,10 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('person')
|
@Table('person')
|
||||||
|
@UpdatedAtTrigger('person_updated_at')
|
||||||
@Check({ name: 'CHK_b0f82b0ed662bfc24fbb58bb45', expression: `"birthDate" <= CURRENT_DATE` })
|
@Check({ name: 'CHK_b0f82b0ed662bfc24fbb58bb45', expression: `"birthDate" <= CURRENT_DATE` })
|
||||||
export class PersonTable {
|
export class PersonTable {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@ -24,31 +25,31 @@ export class PersonTable {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_person_update_id')
|
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId!: string;
|
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@Column({ type: 'date', nullable: true })
|
|
||||||
birthDate!: Date | string | null;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
thumbnailPath!: string;
|
thumbnailPath!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AssetFaceTable, { onDelete: 'SET NULL', nullable: true })
|
|
||||||
faceAssetId!: string | null;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isHidden!: boolean;
|
isHidden!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'date', nullable: true })
|
||||||
|
birthDate!: Date | string | null;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => AssetFaceTable, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
faceAssetId!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true, default: null })
|
@Column({ type: 'character varying', nullable: true, default: null })
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_person_update_id')
|
||||||
|
@UpdateIdColumn()
|
||||||
|
updateId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
@ -7,10 +8,10 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'sessions', primaryConstraintName: 'PK_48cb6b5c20faa63157b3c1baf7f' })
|
@Table({ name: 'sessions', primaryConstraintName: 'PK_48cb6b5c20faa63157b3c1baf7f' })
|
||||||
|
@UpdatedAtTrigger('sessions_updated_at')
|
||||||
export class SessionTable {
|
export class SessionTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
@ -19,22 +20,22 @@ export class SessionTable {
|
|||||||
@Column()
|
@Column()
|
||||||
token!: string;
|
token!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
|
||||||
userId!: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_sessions_update_id')
|
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||||
@UpdateIdColumn()
|
userId!: string;
|
||||||
updateId!: string;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
deviceType!: string;
|
deviceType!: string;
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
deviceOS!: string;
|
deviceOS!: string;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_sessions_update_id')
|
||||||
|
@UpdateIdColumn()
|
||||||
|
updateId!: string;
|
||||||
}
|
}
|
||||||
|
@ -20,16 +20,9 @@ export class SharedLinkTable {
|
|||||||
@Column({ type: 'character varying', nullable: true })
|
@Column({ type: 'character varying', nullable: true })
|
||||||
description!: string | null;
|
description!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true })
|
|
||||||
password!: string | null;
|
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_sharedlink_albumId')
|
|
||||||
@ForeignKeyColumn(() => AlbumTable, { nullable: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
|
||||||
albumId!: string;
|
|
||||||
|
|
||||||
@ColumnIndex('IDX_sharedlink_key')
|
@ColumnIndex('IDX_sharedlink_key')
|
||||||
@Column({ type: 'bytea' })
|
@Column({ type: 'bytea' })
|
||||||
key!: Buffer; // use to access the inidividual asset
|
key!: Buffer; // use to access the inidividual asset
|
||||||
@ -46,9 +39,16 @@ export class SharedLinkTable {
|
|||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
allowUpload!: boolean;
|
allowUpload!: boolean;
|
||||||
|
|
||||||
|
@ColumnIndex('IDX_sharedlink_albumId')
|
||||||
|
@ForeignKeyColumn(() => AlbumTable, { nullable: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
|
albumId!: string;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
allowDownload!: boolean;
|
allowDownload!: boolean;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
showExif!: boolean;
|
showExif!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'character varying', nullable: true })
|
||||||
|
password!: string | null;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { Column, ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
|
import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'smart_search', primaryConstraintName: 'smart_search_pkey' })
|
@Table({ name: 'smart_search', primaryConstraintName: 'smart_search_pkey' })
|
||||||
|
@Index({
|
||||||
|
name: 'clip_index',
|
||||||
|
using: 'hnsw',
|
||||||
|
expression: `embedding vector_cosine_ops`,
|
||||||
|
with: `ef_construction = 300, m = 16`,
|
||||||
|
synchronize: false,
|
||||||
|
})
|
||||||
export class SmartSearchTable {
|
export class SmartSearchTable {
|
||||||
@ForeignKeyColumn(() => AssetTable, {
|
@ForeignKeyColumn(() => AssetTable, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
@ -10,7 +17,6 @@ export class SmartSearchTable {
|
|||||||
})
|
})
|
||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
@ColumnIndex({ name: 'clip_index', synchronize: false })
|
@Column({ type: 'vector', length: 512, storage: 'external', synchronize: false })
|
||||||
@Column({ type: 'vector', array: true, length: 512, synchronize: false })
|
|
||||||
embedding!: string;
|
embedding!: string;
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@ export class StackTable {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
|
||||||
ownerId!: string;
|
|
||||||
|
|
||||||
//TODO: Add constraint to ensure primary asset exists in the assets array
|
//TODO: Add constraint to ensure primary asset exists in the assets array
|
||||||
@ForeignKeyColumn(() => AssetTable, { nullable: false, unique: true })
|
@ForeignKeyColumn(() => AssetTable, { nullable: false, unique: true })
|
||||||
primaryAssetId!: string;
|
primaryAssetId!: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
|
ownerId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { SyncEntityType } from 'src/enum';
|
import { SyncEntityType } from 'src/enum';
|
||||||
import { SessionTable } from 'src/schema/tables/session.table';
|
import { SessionTable } from 'src/schema/tables/session.table';
|
||||||
import {
|
import {
|
||||||
@ -8,10 +9,10 @@ import {
|
|||||||
PrimaryColumn,
|
PrimaryColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('session_sync_checkpoints')
|
@Table('session_sync_checkpoints')
|
||||||
|
@UpdatedAtTrigger('session_sync_checkpoints_updated_at')
|
||||||
export class SessionSyncCheckpointTable {
|
export class SessionSyncCheckpointTable {
|
||||||
@ForeignKeyColumn(() => SessionTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', primary: true })
|
@ForeignKeyColumn(() => SessionTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', primary: true })
|
||||||
sessionId!: string;
|
sessionId!: string;
|
||||||
@ -25,10 +26,10 @@ export class SessionSyncCheckpointTable {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
ack!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_session_sync_checkpoints_update_id')
|
@ColumnIndex('IDX_session_sync_checkpoints_update_id')
|
||||||
@UpdateIdColumn()
|
@UpdateIdColumn()
|
||||||
updateId!: string;
|
updateId!: string;
|
||||||
|
|
||||||
@Column()
|
|
||||||
ack!: string;
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { TagTable } from 'src/schema/tables/tag.table';
|
import { TagTable } from 'src/schema/tables/tag.table';
|
||||||
import { ColumnIndex, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools';
|
import { ColumnIndex, ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('tags_closure')
|
@Table('tags_closure')
|
||||||
export class TagClosureTable {
|
export class TagClosureTable {
|
||||||
@PrimaryColumn()
|
|
||||||
@ColumnIndex()
|
@ColumnIndex()
|
||||||
@ForeignKeyColumn(() => TagTable, { onDelete: 'CASCADE', onUpdate: 'NO ACTION' })
|
@ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION' })
|
||||||
id_ancestor!: string;
|
id_ancestor!: string;
|
||||||
|
|
||||||
@PrimaryColumn()
|
|
||||||
@ColumnIndex()
|
@ColumnIndex()
|
||||||
@ForeignKeyColumn(() => TagTable, { onDelete: 'CASCADE', onUpdate: 'NO ACTION' })
|
@ForeignKeyColumn(() => TagTable, { primary: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION' })
|
||||||
id_descendant!: string;
|
id_descendant!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
@ -8,15 +9,18 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Unique,
|
Unique,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('tags')
|
@Table('tags')
|
||||||
|
@UpdatedAtTrigger('tags_updated_at')
|
||||||
@Unique({ columns: ['userId', 'value'] })
|
@Unique({ columns: ['userId', 'value'] })
|
||||||
export class TagTable {
|
export class TagTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
value!: string;
|
value!: string;
|
||||||
|
|
||||||
@ -26,16 +30,13 @@ export class TagTable {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@ColumnIndex('IDX_tags_update_id')
|
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', nullable: true, default: null })
|
@Column({ type: 'character varying', nullable: true, default: null })
|
||||||
color!: string | null;
|
color!: string | null;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => TagTable, { nullable: true, onDelete: 'CASCADE' })
|
@ForeignKeyColumn(() => TagTable, { nullable: true, onDelete: 'CASCADE' })
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
@ColumnIndex('IDX_tags_update_id')
|
||||||
userId!: string;
|
@UpdateIdColumn()
|
||||||
|
updateId!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { Column, ColumnIndex, CreateDateColumn, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
import { Column, ColumnIndex, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('users_audit')
|
@Table('users_audit')
|
||||||
export class UserAuditTable {
|
export class UserAuditTable {
|
||||||
@PrimaryGeneratedColumn({ type: 'v7' })
|
|
||||||
id!: string;
|
|
||||||
|
|
||||||
@Column({ type: 'uuid' })
|
@Column({ type: 'uuid' })
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
||||||
@ColumnIndex('IDX_users_audit_deleted_at')
|
@ColumnIndex('IDX_users_audit_deleted_at')
|
||||||
@CreateDateColumn({ default: () => 'clock_timestamp()' })
|
@CreateDateColumn({ default: () => 'clock_timestamp()' })
|
||||||
deletedAt!: Date;
|
deletedAt!: Date;
|
||||||
|
|
||||||
|
@PrimaryGeneratedUuidV7Column()
|
||||||
|
id!: string;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { ColumnType } from 'kysely';
|
import { ColumnType } from 'kysely';
|
||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { UserStatus } from 'src/enum';
|
import { UserStatus } from 'src/enum';
|
||||||
|
import { users_delete_audit } from 'src/schema/functions';
|
||||||
import {
|
import {
|
||||||
|
AfterDeleteTrigger,
|
||||||
Column,
|
Column,
|
||||||
ColumnIndex,
|
ColumnIndex,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
@ -9,7 +12,6 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
Table,
|
Table,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
UpdateIdColumn,
|
|
||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||||
@ -17,50 +19,51 @@ type Generated<T> =
|
|||||||
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
|
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
|
||||||
|
|
||||||
@Table('users')
|
@Table('users')
|
||||||
|
@UpdatedAtTrigger('users_updated_at')
|
||||||
|
@AfterDeleteTrigger({
|
||||||
|
name: 'users_delete_audit',
|
||||||
|
scope: 'statement',
|
||||||
|
function: users_delete_audit,
|
||||||
|
referencingOldTableAs: 'old',
|
||||||
|
when: 'pg_trigger_depth() = 0',
|
||||||
|
})
|
||||||
@Index({ name: 'IDX_users_updated_at_asc_id_asc', columns: ['updatedAt', 'id'] })
|
@Index({ name: 'IDX_users_updated_at_asc_id_asc', columns: ['updatedAt', 'id'] })
|
||||||
export class UserTable {
|
export class UserTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: Generated<string>;
|
id!: Generated<string>;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
email!: string;
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
name!: Generated<string>;
|
password!: Generated<string>;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
|
@Column({ default: '' })
|
||||||
|
profileImagePath!: Generated<string>;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
isAdmin!: Generated<boolean>;
|
isAdmin!: Generated<boolean>;
|
||||||
|
|
||||||
@Column({ unique: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
email!: string;
|
shouldChangePassword!: Generated<boolean>;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt!: Timestamp | null;
|
||||||
|
|
||||||
|
@Column({ default: '' })
|
||||||
|
oauthId!: Generated<string>;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
@Column({ unique: true, nullable: true, default: null })
|
@Column({ unique: true, nullable: true, default: null })
|
||||||
storageLabel!: string | null;
|
storageLabel!: string | null;
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
password!: Generated<string>;
|
name!: Generated<string>;
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
oauthId!: Generated<string>;
|
|
||||||
|
|
||||||
@Column({ default: '' })
|
|
||||||
profileImagePath!: Generated<string>;
|
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
|
||||||
shouldChangePassword!: Generated<boolean>;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt!: Generated<Timestamp>;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt!: Generated<Timestamp>;
|
|
||||||
|
|
||||||
@DeleteDateColumn()
|
|
||||||
deletedAt!: Timestamp | null;
|
|
||||||
|
|
||||||
@Column({ type: 'character varying', default: UserStatus.ACTIVE })
|
|
||||||
status!: Generated<UserStatus>;
|
|
||||||
|
|
||||||
@ColumnIndex({ name: 'IDX_users_update_id' })
|
|
||||||
@UpdateIdColumn()
|
|
||||||
updateId!: Generated<string>;
|
|
||||||
|
|
||||||
@Column({ type: 'bigint', nullable: true })
|
@Column({ type: 'bigint', nullable: true })
|
||||||
quotaSizeInBytes!: ColumnType<number> | null;
|
quotaSizeInBytes!: ColumnType<number> | null;
|
||||||
@ -68,6 +71,13 @@ export class UserTable {
|
|||||||
@Column({ type: 'bigint', default: 0 })
|
@Column({ type: 'bigint', default: 0 })
|
||||||
quotaUsageInBytes!: Generated<ColumnType<number>>;
|
quotaUsageInBytes!: Generated<ColumnType<number>>;
|
||||||
|
|
||||||
|
@Column({ type: 'character varying', default: UserStatus.ACTIVE })
|
||||||
|
status!: Generated<UserStatus>;
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', default: () => 'now()' })
|
@Column({ type: 'timestamp with time zone', default: () => 'now()' })
|
||||||
profileChangedAt!: Generated<Timestamp>;
|
profileChangedAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
|
@ColumnIndex({ name: 'IDX_users_update_id' })
|
||||||
|
@UpdateIdColumn()
|
||||||
|
updateId!: Generated<string>;
|
||||||
}
|
}
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
||||||
import { register } from 'src/sql-tools/schema-from-decorators';
|
|
||||||
import {
|
|
||||||
CheckOptions,
|
|
||||||
ColumnDefaultValue,
|
|
||||||
ColumnIndexOptions,
|
|
||||||
ColumnOptions,
|
|
||||||
ForeignKeyColumnOptions,
|
|
||||||
GenerateColumnOptions,
|
|
||||||
IndexOptions,
|
|
||||||
TableOptions,
|
|
||||||
UniqueOptions,
|
|
||||||
} from 'src/sql-tools/types';
|
|
||||||
|
|
||||||
export const Table = (options: string | TableOptions = {}): ClassDecorator => {
|
|
||||||
return (object: Function) => void register({ type: 'table', item: { object, options: asOptions(options) } });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => {
|
|
||||||
return (object: object, propertyName: string | symbol) =>
|
|
||||||
void register({ type: 'column', item: { object, propertyName, options: asOptions(options) } });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Index = (options: string | IndexOptions = {}): ClassDecorator => {
|
|
||||||
return (object: Function) => void register({ type: 'index', item: { object, options: asOptions(options) } });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Unique = (options: UniqueOptions): ClassDecorator => {
|
|
||||||
return (object: Function) => void register({ type: 'uniqueConstraint', item: { object, options } });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Check = (options: CheckOptions): ClassDecorator => {
|
|
||||||
return (object: Function) => void register({ type: 'checkConstraint', item: { object, options } });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ColumnIndex = (options: string | ColumnIndexOptions = {}): PropertyDecorator => {
|
|
||||||
return (object: object, propertyName: string | symbol) =>
|
|
||||||
void register({ type: 'columnIndex', item: { object, propertyName, options: asOptions(options) } });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ForeignKeyColumn = (target: () => object, options: ForeignKeyColumnOptions): PropertyDecorator => {
|
|
||||||
return (object: object, propertyName: string | symbol) => {
|
|
||||||
register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } });
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
|
||||||
return Column({
|
|
||||||
type: 'timestamp with time zone',
|
|
||||||
default: () => 'now()',
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
|
||||||
return Column({
|
|
||||||
type: 'timestamp with time zone',
|
|
||||||
default: () => 'now()',
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
|
||||||
return Column({
|
|
||||||
type: 'timestamp with time zone',
|
|
||||||
nullable: true,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PrimaryGeneratedColumn = (options: Omit<GenerateColumnOptions, 'primary'> = {}) =>
|
|
||||||
GeneratedColumn({ type: 'v4', ...options, primary: true });
|
|
||||||
|
|
||||||
export const PrimaryColumn = (options: Omit<ColumnOptions, 'primary'> = {}) => Column({ ...options, primary: true });
|
|
||||||
|
|
||||||
export const GeneratedColumn = ({ type = 'v4', ...options }: GenerateColumnOptions): PropertyDecorator => {
|
|
||||||
const columnType = type === 'v4' || type === 'v7' ? 'uuid' : type;
|
|
||||||
|
|
||||||
let columnDefault: ColumnDefaultValue | undefined;
|
|
||||||
switch (type) {
|
|
||||||
case 'v4': {
|
|
||||||
columnDefault = () => 'uuid_generate_v4()';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'v7': {
|
|
||||||
columnDefault = () => 'immich_uuid_v7()';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column({
|
|
||||||
type: columnType,
|
|
||||||
default: columnDefault,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateIdColumn = () => GeneratedColumn({ type: 'v7', nullable: false });
|
|
||||||
|
|
||||||
const asOptions = <T extends { name?: string }>(options: string | T): T => {
|
|
||||||
if (typeof options === 'string') {
|
|
||||||
return { name: options } as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
};
|
|
81
server/src/sql-tools/diff/comparers/column.comparer.spec.ts
Normal file
81
server/src/sql-tools/diff/comparers/column.comparer.spec.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { compareColumns } from 'src/sql-tools/diff/comparers/column.comparer';
|
||||||
|
import { DatabaseColumn, Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testColumn: DatabaseColumn = {
|
||||||
|
name: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
nullable: false,
|
||||||
|
isArray: false,
|
||||||
|
type: 'character varying',
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('compareColumns', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareColumns.onExtra(testColumn)).toEqual([
|
||||||
|
{
|
||||||
|
tableName: 'table1',
|
||||||
|
columnName: 'test',
|
||||||
|
type: 'column.drop',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareColumns.onMissing(testColumn)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'column.add',
|
||||||
|
column: testColumn,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareColumns.onCompare(testColumn, testColumn)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change in type', () => {
|
||||||
|
const source: DatabaseColumn = { ...testColumn };
|
||||||
|
const target: DatabaseColumn = { ...testColumn, type: 'text' };
|
||||||
|
const reason = 'column type is different (character varying vs text)';
|
||||||
|
expect(compareColumns.onCompare(source, target)).toEqual([
|
||||||
|
{
|
||||||
|
columnName: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'column.drop',
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'column.add',
|
||||||
|
column: source,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a comment change', () => {
|
||||||
|
const source: DatabaseColumn = { ...testColumn, comment: 'new comment' };
|
||||||
|
const target: DatabaseColumn = { ...testColumn, comment: 'old comment' };
|
||||||
|
const reason = 'comment is different (new comment vs old comment)';
|
||||||
|
expect(compareColumns.onCompare(source, target)).toEqual([
|
||||||
|
{
|
||||||
|
columnName: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'column.alter',
|
||||||
|
changes: {
|
||||||
|
comment: 'new comment',
|
||||||
|
},
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
82
server/src/sql-tools/diff/comparers/column.comparer.ts
Normal file
82
server/src/sql-tools/diff/comparers/column.comparer.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { getColumnType, isDefaultEqual } from 'src/sql-tools/helpers';
|
||||||
|
import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareColumns: Comparer<DatabaseColumn> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'column.add',
|
||||||
|
column: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
{
|
||||||
|
type: 'column.drop',
|
||||||
|
tableName: target.tableName,
|
||||||
|
columnName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: (source, target) => {
|
||||||
|
const sourceType = getColumnType(source);
|
||||||
|
const targetType = getColumnType(target);
|
||||||
|
|
||||||
|
const isTypeChanged = sourceType !== targetType;
|
||||||
|
|
||||||
|
if (isTypeChanged) {
|
||||||
|
// TODO: convert between types via UPDATE when possible
|
||||||
|
return dropAndRecreateColumn(source, target, `column type is different (${sourceType} vs ${targetType})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: SchemaDiff[] = [];
|
||||||
|
if (source.nullable !== target.nullable) {
|
||||||
|
items.push({
|
||||||
|
type: 'column.alter',
|
||||||
|
tableName: source.tableName,
|
||||||
|
columnName: source.name,
|
||||||
|
changes: {
|
||||||
|
nullable: source.nullable,
|
||||||
|
},
|
||||||
|
reason: `nullable is different (${source.nullable} vs ${target.nullable})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDefaultEqual(source, target)) {
|
||||||
|
items.push({
|
||||||
|
type: 'column.alter',
|
||||||
|
tableName: source.tableName,
|
||||||
|
columnName: source.name,
|
||||||
|
changes: {
|
||||||
|
default: String(source.default),
|
||||||
|
},
|
||||||
|
reason: `default is different (${source.default} vs ${target.default})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.comment !== target.comment) {
|
||||||
|
items.push({
|
||||||
|
type: 'column.alter',
|
||||||
|
tableName: source.tableName,
|
||||||
|
columnName: source.name,
|
||||||
|
changes: {
|
||||||
|
comment: String(source.comment),
|
||||||
|
},
|
||||||
|
reason: `comment is different (${source.comment} vs ${target.comment})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'column.drop',
|
||||||
|
tableName: target.tableName,
|
||||||
|
columnName: target.name,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
{ type: 'column.add', column: source, reason },
|
||||||
|
];
|
||||||
|
};
|
@ -0,0 +1,63 @@
|
|||||||
|
import { compareConstraints } from 'src/sql-tools/diff/comparers/constraint.comparer';
|
||||||
|
import { DatabaseConstraint, DatabaseConstraintType, Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testConstraint: DatabaseConstraint = {
|
||||||
|
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||||
|
name: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['column1'],
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('compareConstraints', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareConstraints.onExtra(testConstraint)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'constraint.drop',
|
||||||
|
constraintName: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareConstraints.onMissing(testConstraint)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'constraint.add',
|
||||||
|
constraint: testConstraint,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareConstraints.onCompare(testConstraint, testConstraint)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change in type', () => {
|
||||||
|
const source: DatabaseConstraint = { ...testConstraint };
|
||||||
|
const target: DatabaseConstraint = { ...testConstraint, columnNames: ['column1', 'column2'] };
|
||||||
|
const reason = 'Primary key columns are different: (column1 vs column1,column2)';
|
||||||
|
expect(compareConstraints.onCompare(source, target)).toEqual([
|
||||||
|
{
|
||||||
|
constraintName: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
type: 'constraint.drop',
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'constraint.add',
|
||||||
|
constraint: source,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
133
server/src/sql-tools/diff/comparers/constraint.comparer.ts
Normal file
133
server/src/sql-tools/diff/comparers/constraint.comparer.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { haveEqualColumns } from 'src/sql-tools/helpers';
|
||||||
|
import {
|
||||||
|
CompareFunction,
|
||||||
|
Comparer,
|
||||||
|
DatabaseCheckConstraint,
|
||||||
|
DatabaseConstraint,
|
||||||
|
DatabaseConstraintType,
|
||||||
|
DatabaseForeignKeyConstraint,
|
||||||
|
DatabasePrimaryKeyConstraint,
|
||||||
|
DatabaseUniqueConstraint,
|
||||||
|
Reason,
|
||||||
|
SchemaDiff,
|
||||||
|
} from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareConstraints: Comparer<DatabaseConstraint> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'constraint.add',
|
||||||
|
constraint: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
{
|
||||||
|
type: 'constraint.drop',
|
||||||
|
tableName: target.tableName,
|
||||||
|
constraintName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: (source, target) => {
|
||||||
|
switch (source.type) {
|
||||||
|
case DatabaseConstraintType.PRIMARY_KEY: {
|
||||||
|
return comparePrimaryKeyConstraint(source, target as DatabasePrimaryKeyConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
case DatabaseConstraintType.FOREIGN_KEY: {
|
||||||
|
return compareForeignKeyConstraint(source, target as DatabaseForeignKeyConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
case DatabaseConstraintType.UNIQUE: {
|
||||||
|
return compareUniqueConstraint(source, target as DatabaseUniqueConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
case DatabaseConstraintType.CHECK: {
|
||||||
|
return compareCheckConstraint(source, target as DatabaseCheckConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const comparePrimaryKeyConstraint: CompareFunction<DatabasePrimaryKeyConstraint> = (source, target) => {
|
||||||
|
if (!haveEqualColumns(source.columnNames, target.columnNames)) {
|
||||||
|
return dropAndRecreateConstraint(
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
`Primary key columns are different: (${source.columnNames} vs ${target.columnNames})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareForeignKeyConstraint: CompareFunction<DatabaseForeignKeyConstraint> = (source, target) => {
|
||||||
|
let reason = '';
|
||||||
|
|
||||||
|
const sourceDeleteAction = source.onDelete ?? 'NO ACTION';
|
||||||
|
const targetDeleteAction = target.onDelete ?? 'NO ACTION';
|
||||||
|
|
||||||
|
const sourceUpdateAction = source.onUpdate ?? 'NO ACTION';
|
||||||
|
const targetUpdateAction = target.onUpdate ?? 'NO ACTION';
|
||||||
|
|
||||||
|
if (!haveEqualColumns(source.columnNames, target.columnNames)) {
|
||||||
|
reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
|
||||||
|
} else if (!haveEqualColumns(source.referenceColumnNames, target.referenceColumnNames)) {
|
||||||
|
reason = `reference columns are different (${source.referenceColumnNames} vs ${target.referenceColumnNames})`;
|
||||||
|
} else if (source.referenceTableName !== target.referenceTableName) {
|
||||||
|
reason = `reference table is different (${source.referenceTableName} vs ${target.referenceTableName})`;
|
||||||
|
} else if (sourceDeleteAction !== targetDeleteAction) {
|
||||||
|
reason = `ON DELETE action is different (${sourceDeleteAction} vs ${targetDeleteAction})`;
|
||||||
|
} else if (sourceUpdateAction !== targetUpdateAction) {
|
||||||
|
reason = `ON UPDATE action is different (${sourceUpdateAction} vs ${targetUpdateAction})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
return dropAndRecreateConstraint(source, target, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareUniqueConstraint: CompareFunction<DatabaseUniqueConstraint> = (source, target) => {
|
||||||
|
let reason = '';
|
||||||
|
|
||||||
|
if (!haveEqualColumns(source.columnNames, target.columnNames)) {
|
||||||
|
reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
return dropAndRecreateConstraint(source, target, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareCheckConstraint: CompareFunction<DatabaseCheckConstraint> = (source, target) => {
|
||||||
|
if (source.expression !== target.expression) {
|
||||||
|
// comparing expressions is hard because postgres reconstructs it with different formatting
|
||||||
|
// for now if the constraint exists with the same name, we will just skip it
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropAndRecreateConstraint = (
|
||||||
|
source: DatabaseConstraint,
|
||||||
|
target: DatabaseConstraint,
|
||||||
|
reason: string,
|
||||||
|
): SchemaDiff[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'constraint.drop',
|
||||||
|
tableName: target.tableName,
|
||||||
|
constraintName: target.name,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
{ type: 'constraint.add', constraint: source, reason },
|
||||||
|
];
|
||||||
|
};
|
54
server/src/sql-tools/diff/comparers/enum.comparer.spec.ts
Normal file
54
server/src/sql-tools/diff/comparers/enum.comparer.spec.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { compareEnums } from 'src/sql-tools/diff/comparers/enum.comparer';
|
||||||
|
import { DatabaseEnum, Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testEnum: DatabaseEnum = { name: 'test', values: ['foo', 'bar'], synchronize: true };
|
||||||
|
|
||||||
|
describe('compareEnums', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareEnums.onExtra(testEnum)).toEqual([
|
||||||
|
{
|
||||||
|
enumName: 'test',
|
||||||
|
type: 'enum.drop',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareEnums.onMissing(testEnum)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'enum.create',
|
||||||
|
enum: testEnum,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareEnums.onCompare(testEnum, testEnum)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should drop and recreate when values list is different', () => {
|
||||||
|
const source = { name: 'test', values: ['foo', 'bar'], synchronize: true };
|
||||||
|
const target = { name: 'test', values: ['foo', 'bar', 'world'], synchronize: true };
|
||||||
|
expect(compareEnums.onCompare(source, target)).toEqual([
|
||||||
|
{
|
||||||
|
enumName: 'test',
|
||||||
|
type: 'enum.drop',
|
||||||
|
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'enum.create',
|
||||||
|
enum: source,
|
||||||
|
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
38
server/src/sql-tools/diff/comparers/enum.comparer.ts
Normal file
38
server/src/sql-tools/diff/comparers/enum.comparer.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Comparer, DatabaseEnum, Reason } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareEnums: Comparer<DatabaseEnum> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'enum.create',
|
||||||
|
enum: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
{
|
||||||
|
type: 'enum.drop',
|
||||||
|
enumName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: (source, target) => {
|
||||||
|
if (source.values.toString() !== target.values.toString()) {
|
||||||
|
// TODO add or remove values if the lists are different or the order has changed
|
||||||
|
const reason = `enum values has changed (${source.values} vs ${target.values})`;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'enum.drop',
|
||||||
|
enumName: source.name,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'enum.create',
|
||||||
|
enum: source,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
import { compareExtensions } from 'src/sql-tools/diff/comparers/extension.comparer';
|
||||||
|
import { Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testExtension = { name: 'test', synchronize: true };
|
||||||
|
|
||||||
|
describe('compareExtensions', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareExtensions.onExtra(testExtension)).toEqual([
|
||||||
|
{
|
||||||
|
extensionName: 'test',
|
||||||
|
type: 'extension.drop',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareExtensions.onMissing(testExtension)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'extension.create',
|
||||||
|
extension: testExtension,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareExtensions.onCompare(testExtension, testExtension)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
22
server/src/sql-tools/diff/comparers/extension.comparer.ts
Normal file
22
server/src/sql-tools/diff/comparers/extension.comparer.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Comparer, DatabaseExtension, Reason } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareExtensions: Comparer<DatabaseExtension> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'extension.create',
|
||||||
|
extension: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
{
|
||||||
|
type: 'extension.drop',
|
||||||
|
extensionName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: () => {
|
||||||
|
// if the name matches they are the same
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,53 @@
|
|||||||
|
import { compareFunctions } from 'src/sql-tools/diff/comparers/function.comparer';
|
||||||
|
import { DatabaseFunction, Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testFunction: DatabaseFunction = {
|
||||||
|
name: 'test',
|
||||||
|
expression: 'CREATE FUNCTION something something something',
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('compareFunctions', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareFunctions.onExtra(testFunction)).toEqual([
|
||||||
|
{
|
||||||
|
functionName: 'test',
|
||||||
|
type: 'function.drop',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareFunctions.onMissing(testFunction)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'function.create',
|
||||||
|
function: testFunction,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should ignore functions with the same hash', () => {
|
||||||
|
expect(compareFunctions.onCompare(testFunction, testFunction)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report differences if functions have different hashes', () => {
|
||||||
|
const source: DatabaseFunction = { ...testFunction, expression: 'SELECT 1' };
|
||||||
|
const target: DatabaseFunction = { ...testFunction, expression: 'SELECT 2' };
|
||||||
|
expect(compareFunctions.onCompare(source, target)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'function.create',
|
||||||
|
reason: 'function expression has changed (SELECT 1 vs SELECT 2)',
|
||||||
|
function: source,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
32
server/src/sql-tools/diff/comparers/function.comparer.ts
Normal file
32
server/src/sql-tools/diff/comparers/function.comparer.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Comparer, DatabaseFunction, Reason } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareFunctions: Comparer<DatabaseFunction> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'function.create',
|
||||||
|
function: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
{
|
||||||
|
type: 'function.drop',
|
||||||
|
functionName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: (source, target) => {
|
||||||
|
if (source.expression !== target.expression) {
|
||||||
|
const reason = `function expression has changed (${source.expression} vs ${target.expression})`;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'function.create',
|
||||||
|
function: source,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
72
server/src/sql-tools/diff/comparers/index.comparer.spec.ts
Normal file
72
server/src/sql-tools/diff/comparers/index.comparer.spec.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { compareIndexes } from 'src/sql-tools/diff/comparers/index.comparer';
|
||||||
|
import { DatabaseIndex, Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testIndex: DatabaseIndex = {
|
||||||
|
name: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['column1', 'column2'],
|
||||||
|
unique: false,
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('compareIndexes', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareIndexes.onExtra(testIndex)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'index.drop',
|
||||||
|
indexName: 'test',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareIndexes.onMissing(testIndex)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'index.create',
|
||||||
|
index: testIndex,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareIndexes.onCompare(testIndex, testIndex)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should drop and recreate when column list is different', () => {
|
||||||
|
const source = {
|
||||||
|
name: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['column1'],
|
||||||
|
unique: true,
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
const target = {
|
||||||
|
name: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
columnNames: ['column1', 'column2'],
|
||||||
|
unique: true,
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
expect(compareIndexes.onCompare(source, target)).toEqual([
|
||||||
|
{
|
||||||
|
indexName: 'test',
|
||||||
|
type: 'index.drop',
|
||||||
|
reason: 'columns are different (column1 vs column1,column2)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'index.create',
|
||||||
|
index: source,
|
||||||
|
reason: 'columns are different (column1 vs column1,column2)',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
46
server/src/sql-tools/diff/comparers/index.comparer.ts
Normal file
46
server/src/sql-tools/diff/comparers/index.comparer.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { haveEqualColumns } from 'src/sql-tools/helpers';
|
||||||
|
import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareIndexes: Comparer<DatabaseIndex> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'index.create',
|
||||||
|
index: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
{
|
||||||
|
type: 'index.drop',
|
||||||
|
indexName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: (source, target) => {
|
||||||
|
const sourceUsing = source.using ?? 'btree';
|
||||||
|
const targetUsing = target.using ?? 'btree';
|
||||||
|
|
||||||
|
let reason = '';
|
||||||
|
|
||||||
|
if (!haveEqualColumns(source.columnNames, target.columnNames)) {
|
||||||
|
reason = `columns are different (${source.columnNames} vs ${target.columnNames})`;
|
||||||
|
} else if (source.unique !== target.unique) {
|
||||||
|
reason = `uniqueness is different (${source.unique} vs ${target.unique})`;
|
||||||
|
} else if (sourceUsing !== targetUsing) {
|
||||||
|
reason = `using method is different (${source.using} vs ${target.using})`;
|
||||||
|
} else if (source.where !== target.where) {
|
||||||
|
reason = `where clause is different (${source.where} vs ${target.where})`;
|
||||||
|
} else if (source.expression !== target.expression) {
|
||||||
|
reason = `expression is different (${source.expression} vs ${target.expression})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
return [
|
||||||
|
{ type: 'index.drop', indexName: target.name, reason },
|
||||||
|
{ type: 'index.create', index: source, reason },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,44 @@
|
|||||||
|
import { compareParameters } from 'src/sql-tools/diff/comparers/parameter.comparer';
|
||||||
|
import { DatabaseParameter, Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testParameter: DatabaseParameter = {
|
||||||
|
name: 'test',
|
||||||
|
databaseName: 'immich',
|
||||||
|
value: 'on',
|
||||||
|
scope: 'database',
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('compareParameters', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareParameters.onExtra(testParameter)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'parameter.reset',
|
||||||
|
databaseName: 'immich',
|
||||||
|
parameterName: 'test',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareParameters.onMissing(testParameter)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'parameter.set',
|
||||||
|
parameter: testParameter,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareParameters.onCompare(testParameter, testParameter)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
23
server/src/sql-tools/diff/comparers/parameter.comparer.ts
Normal file
23
server/src/sql-tools/diff/comparers/parameter.comparer.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Comparer, DatabaseParameter, Reason } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareParameters: Comparer<DatabaseParameter> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'parameter.set',
|
||||||
|
parameter: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
{
|
||||||
|
type: 'parameter.reset',
|
||||||
|
databaseName: target.databaseName,
|
||||||
|
parameterName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: () => {
|
||||||
|
// TODO
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
44
server/src/sql-tools/diff/comparers/table.comparer.spec.ts
Normal file
44
server/src/sql-tools/diff/comparers/table.comparer.spec.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { compareTables } from 'src/sql-tools/diff/comparers/table.comparer';
|
||||||
|
import { DatabaseTable, Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testTable: DatabaseTable = {
|
||||||
|
name: 'test',
|
||||||
|
columns: [],
|
||||||
|
constraints: [],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('compareParameters', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareTables.onExtra(testTable)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'table.drop',
|
||||||
|
tableName: 'test',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareTables.onMissing(testTable)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'table.create',
|
||||||
|
table: testTable,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareTables.onCompare(testTable, testTable)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
59
server/src/sql-tools/diff/comparers/table.comparer.ts
Normal file
59
server/src/sql-tools/diff/comparers/table.comparer.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { compareColumns } from 'src/sql-tools/diff/comparers/column.comparer';
|
||||||
|
import { compareConstraints } from 'src/sql-tools/diff/comparers/constraint.comparer';
|
||||||
|
import { compareIndexes } from 'src/sql-tools/diff/comparers/index.comparer';
|
||||||
|
import { compareTriggers } from 'src/sql-tools/diff/comparers/trigger.comparer';
|
||||||
|
import { compare } from 'src/sql-tools/helpers';
|
||||||
|
import { Comparer, DatabaseTable, Reason, SchemaDiff } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareTables: Comparer<DatabaseTable> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'table.create',
|
||||||
|
table: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
// TODO merge constraints into table create record when possible
|
||||||
|
...compareTable(
|
||||||
|
source,
|
||||||
|
{
|
||||||
|
name: source.name,
|
||||||
|
columns: [],
|
||||||
|
indexes: [],
|
||||||
|
constraints: [],
|
||||||
|
triggers: [],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{ columns: false },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
...compareTable(
|
||||||
|
{
|
||||||
|
name: target.name,
|
||||||
|
columns: [],
|
||||||
|
indexes: [],
|
||||||
|
constraints: [],
|
||||||
|
triggers: [],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
|
target,
|
||||||
|
{ columns: false },
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: 'table.drop',
|
||||||
|
tableName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: (source, target) => compareTable(source, target, { columns: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareTable = (source: DatabaseTable, target: DatabaseTable, options: { columns?: boolean }): SchemaDiff[] => {
|
||||||
|
return [
|
||||||
|
...(options.columns ? compare(source.columns, target.columns, {}, compareColumns) : []),
|
||||||
|
...compare(source.indexes, target.indexes, {}, compareIndexes),
|
||||||
|
...compare(source.constraints, target.constraints, {}, compareConstraints),
|
||||||
|
...compare(source.triggers, target.triggers, {}, compareTriggers),
|
||||||
|
];
|
||||||
|
};
|
88
server/src/sql-tools/diff/comparers/trigger.comparer.spec.ts
Normal file
88
server/src/sql-tools/diff/comparers/trigger.comparer.spec.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { compareTriggers } from 'src/sql-tools/diff/comparers/trigger.comparer';
|
||||||
|
import { DatabaseTrigger, Reason } from 'src/sql-tools/types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const testTrigger: DatabaseTrigger = {
|
||||||
|
name: 'test',
|
||||||
|
tableName: 'table1',
|
||||||
|
timing: 'before',
|
||||||
|
actions: ['delete'],
|
||||||
|
scope: 'row',
|
||||||
|
functionName: 'my_trigger_function',
|
||||||
|
synchronize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('compareTriggers', () => {
|
||||||
|
describe('onExtra', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareTriggers.onExtra(testTrigger)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'trigger.drop',
|
||||||
|
tableName: 'table1',
|
||||||
|
triggerName: 'test',
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onMissing', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareTriggers.onMissing(testTrigger)).toEqual([
|
||||||
|
{
|
||||||
|
type: 'trigger.create',
|
||||||
|
trigger: testTrigger,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCompare', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(compareTriggers.onCompare(testTrigger, testTrigger)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change in function name', () => {
|
||||||
|
const source: DatabaseTrigger = { ...testTrigger, functionName: 'my_new_name' };
|
||||||
|
const target: DatabaseTrigger = { ...testTrigger, functionName: 'my_old_name' };
|
||||||
|
const reason = `function is different (my_new_name vs my_old_name)`;
|
||||||
|
expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change in actions', () => {
|
||||||
|
const source: DatabaseTrigger = { ...testTrigger, actions: ['delete'] };
|
||||||
|
const target: DatabaseTrigger = { ...testTrigger, actions: ['delete', 'insert'] };
|
||||||
|
const reason = `action is different (delete vs delete,insert)`;
|
||||||
|
expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change in timing', () => {
|
||||||
|
const source: DatabaseTrigger = { ...testTrigger, timing: 'before' };
|
||||||
|
const target: DatabaseTrigger = { ...testTrigger, timing: 'after' };
|
||||||
|
const reason = `timing method is different (before vs after)`;
|
||||||
|
expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change in scope', () => {
|
||||||
|
const source: DatabaseTrigger = { ...testTrigger, scope: 'row' };
|
||||||
|
const target: DatabaseTrigger = { ...testTrigger, scope: 'statement' };
|
||||||
|
const reason = `scope is different (row vs statement)`;
|
||||||
|
expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change in new table reference', () => {
|
||||||
|
const source: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: 'new_table' };
|
||||||
|
const target: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: undefined };
|
||||||
|
const reason = `new table reference is different (new_table vs undefined)`;
|
||||||
|
expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect a change in old table reference', () => {
|
||||||
|
const source: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: 'old_table' };
|
||||||
|
const target: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: undefined };
|
||||||
|
const reason = `old table reference is different (old_table vs undefined)`;
|
||||||
|
expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'trigger.create', trigger: source, reason }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
41
server/src/sql-tools/diff/comparers/trigger.comparer.ts
Normal file
41
server/src/sql-tools/diff/comparers/trigger.comparer.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Comparer, DatabaseTrigger, Reason } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const compareTriggers: Comparer<DatabaseTrigger> = {
|
||||||
|
onMissing: (source) => [
|
||||||
|
{
|
||||||
|
type: 'trigger.create',
|
||||||
|
trigger: source,
|
||||||
|
reason: Reason.MissingInTarget,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onExtra: (target) => [
|
||||||
|
{
|
||||||
|
type: 'trigger.drop',
|
||||||
|
tableName: target.tableName,
|
||||||
|
triggerName: target.name,
|
||||||
|
reason: Reason.MissingInSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onCompare: (source, target) => {
|
||||||
|
let reason = '';
|
||||||
|
if (source.functionName !== target.functionName) {
|
||||||
|
reason = `function is different (${source.functionName} vs ${target.functionName})`;
|
||||||
|
} else if (source.actions.join(' OR ') !== target.actions.join(' OR ')) {
|
||||||
|
reason = `action is different (${source.actions} vs ${target.actions})`;
|
||||||
|
} else if (source.timing !== target.timing) {
|
||||||
|
reason = `timing method is different (${source.timing} vs ${target.timing})`;
|
||||||
|
} else if (source.scope !== target.scope) {
|
||||||
|
reason = `scope is different (${source.scope} vs ${target.scope})`;
|
||||||
|
} else if (source.referencingNewTableAs !== target.referencingNewTableAs) {
|
||||||
|
reason = `new table reference is different (${source.referencingNewTableAs} vs ${target.referencingNewTableAs})`;
|
||||||
|
} else if (source.referencingOldTableAs !== target.referencingOldTableAs) {
|
||||||
|
reason = `old table reference is different (${source.referencingOldTableAs} vs ${target.referencingOldTableAs})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
return [{ type: 'trigger.create', trigger: source, reason }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
@ -1,8 +1,8 @@
|
|||||||
import { schemaDiff } from 'src/sql-tools/schema-diff';
|
import { schemaDiff } from 'src/sql-tools/diff';
|
||||||
import {
|
import {
|
||||||
|
ColumnType,
|
||||||
DatabaseActionType,
|
DatabaseActionType,
|
||||||
DatabaseColumn,
|
DatabaseColumn,
|
||||||
DatabaseColumnType,
|
|
||||||
DatabaseConstraint,
|
DatabaseConstraint,
|
||||||
DatabaseConstraintType,
|
DatabaseConstraintType,
|
||||||
DatabaseIndex,
|
DatabaseIndex,
|
||||||
@ -15,7 +15,12 @@ const fromColumn = (column: Partial<Omit<DatabaseColumn, 'tableName'>>): Databas
|
|||||||
const tableName = 'table1';
|
const tableName = 'table1';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'public',
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
name: tableName,
|
name: tableName,
|
||||||
@ -31,6 +36,7 @@ const fromColumn = (column: Partial<Omit<DatabaseColumn, 'tableName'>>): Databas
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
indexes: [],
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
constraints: [],
|
constraints: [],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
},
|
},
|
||||||
@ -43,7 +49,12 @@ const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => {
|
|||||||
const tableName = constraint?.tableName || 'table1';
|
const tableName = constraint?.tableName || 'table1';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'public',
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
name: tableName,
|
name: tableName,
|
||||||
@ -58,6 +69,7 @@ const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
indexes: [],
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
constraints: constraint ? [constraint] : [],
|
constraints: constraint ? [constraint] : [],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
},
|
},
|
||||||
@ -70,7 +82,12 @@ const fromIndex = (index?: DatabaseIndex): DatabaseSchema => {
|
|||||||
const tableName = index?.tableName || 'table1';
|
const tableName = index?.tableName || 'table1';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'public',
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
name: tableName,
|
name: tableName,
|
||||||
@ -86,6 +103,7 @@ const fromIndex = (index?: DatabaseIndex): DatabaseSchema => {
|
|||||||
],
|
],
|
||||||
indexes: index ? [index] : [],
|
indexes: index ? [index] : [],
|
||||||
constraints: [],
|
constraints: [],
|
||||||
|
triggers: [],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -99,7 +117,7 @@ const newSchema = (schema: {
|
|||||||
name: string;
|
name: string;
|
||||||
columns?: Array<{
|
columns?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
type?: DatabaseColumnType;
|
type?: ColumnType;
|
||||||
nullable?: boolean;
|
nullable?: boolean;
|
||||||
isArray?: boolean;
|
isArray?: boolean;
|
||||||
}>;
|
}>;
|
||||||
@ -131,12 +149,18 @@ const newSchema = (schema: {
|
|||||||
columns,
|
columns,
|
||||||
indexes: table.indexes ?? [],
|
indexes: table.indexes ?? [],
|
||||||
constraints: table.constraints ?? [],
|
constraints: table.constraints ?? [],
|
||||||
|
triggers: [],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: schema?.name || 'public',
|
name: 'immich',
|
||||||
|
schemaName: schema?.name || 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
tables,
|
tables,
|
||||||
warnings: [],
|
warnings: [],
|
||||||
};
|
};
|
||||||
@ -167,8 +191,14 @@ describe('schemaDiff', () => {
|
|||||||
expect(diff.items).toHaveLength(1);
|
expect(diff.items).toHaveLength(1);
|
||||||
expect(diff.items[0]).toEqual({
|
expect(diff.items[0]).toEqual({
|
||||||
type: 'table.create',
|
type: 'table.create',
|
||||||
tableName: 'table1',
|
table: {
|
||||||
columns: [column],
|
name: 'table1',
|
||||||
|
columns: [column],
|
||||||
|
constraints: [],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
synchronize: true,
|
||||||
|
},
|
||||||
reason: 'missing in target',
|
reason: 'missing in target',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -181,7 +211,7 @@ describe('schemaDiff', () => {
|
|||||||
newSchema({
|
newSchema({
|
||||||
tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
|
tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
|
||||||
}),
|
}),
|
||||||
{ ignoreExtraTables: false },
|
{ tables: { ignoreExtra: false } },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(diff.items).toHaveLength(1);
|
expect(diff.items).toHaveLength(1);
|
85
server/src/sql-tools/diff/index.ts
Normal file
85
server/src/sql-tools/diff/index.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { compareEnums } from 'src/sql-tools/diff/comparers/enum.comparer';
|
||||||
|
import { compareExtensions } from 'src/sql-tools/diff/comparers/extension.comparer';
|
||||||
|
import { compareFunctions } from 'src/sql-tools/diff/comparers/function.comparer';
|
||||||
|
import { compareParameters } from 'src/sql-tools/diff/comparers/parameter.comparer';
|
||||||
|
import { compareTables } from 'src/sql-tools/diff/comparers/table.comparer';
|
||||||
|
import { compare } from 'src/sql-tools/helpers';
|
||||||
|
import { schemaDiffToSql } from 'src/sql-tools/to-sql';
|
||||||
|
import {
|
||||||
|
DatabaseConstraintType,
|
||||||
|
DatabaseSchema,
|
||||||
|
SchemaDiff,
|
||||||
|
SchemaDiffOptions,
|
||||||
|
SchemaDiffToSqlOptions,
|
||||||
|
} from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the difference between two database schemas
|
||||||
|
*/
|
||||||
|
export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, options: SchemaDiffOptions = {}) => {
|
||||||
|
const items = [
|
||||||
|
...compare(source.parameters, target.parameters, options.parameters, compareParameters),
|
||||||
|
...compare(source.extensions, target.extensions, options.extension, compareExtensions),
|
||||||
|
...compare(source.functions, target.functions, options.functions, compareFunctions),
|
||||||
|
...compare(source.enums, target.enums, options.enums, compareEnums),
|
||||||
|
...compare(source.tables, target.tables, options.tables, compareTables),
|
||||||
|
];
|
||||||
|
|
||||||
|
type SchemaName = SchemaDiff['type'];
|
||||||
|
const itemMap: Record<SchemaName, SchemaDiff[]> = {
|
||||||
|
'enum.create': [],
|
||||||
|
'enum.drop': [],
|
||||||
|
'extension.create': [],
|
||||||
|
'extension.drop': [],
|
||||||
|
'function.create': [],
|
||||||
|
'function.drop': [],
|
||||||
|
'table.create': [],
|
||||||
|
'table.drop': [],
|
||||||
|
'column.add': [],
|
||||||
|
'column.alter': [],
|
||||||
|
'column.drop': [],
|
||||||
|
'constraint.add': [],
|
||||||
|
'constraint.drop': [],
|
||||||
|
'index.create': [],
|
||||||
|
'index.drop': [],
|
||||||
|
'trigger.create': [],
|
||||||
|
'trigger.drop': [],
|
||||||
|
'parameter.set': [],
|
||||||
|
'parameter.reset': [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
itemMap[item.type].push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const constraintAdds = itemMap['constraint.add'].filter((item) => item.type === 'constraint.add');
|
||||||
|
|
||||||
|
const orderedItems = [
|
||||||
|
...itemMap['extension.create'],
|
||||||
|
...itemMap['function.create'],
|
||||||
|
...itemMap['parameter.set'],
|
||||||
|
...itemMap['parameter.reset'],
|
||||||
|
...itemMap['enum.create'],
|
||||||
|
...itemMap['trigger.drop'],
|
||||||
|
...itemMap['index.drop'],
|
||||||
|
...itemMap['constraint.drop'],
|
||||||
|
...itemMap['table.create'],
|
||||||
|
...itemMap['column.alter'],
|
||||||
|
...itemMap['column.add'],
|
||||||
|
...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.PRIMARY_KEY),
|
||||||
|
...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.FOREIGN_KEY),
|
||||||
|
...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.UNIQUE),
|
||||||
|
...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.CHECK),
|
||||||
|
...itemMap['index.create'],
|
||||||
|
...itemMap['trigger.create'],
|
||||||
|
...itemMap['column.drop'],
|
||||||
|
...itemMap['table.drop'],
|
||||||
|
...itemMap['enum.drop'],
|
||||||
|
...itemMap['function.drop'],
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: orderedItems,
|
||||||
|
asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options),
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,8 @@
|
|||||||
|
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||||
|
|
||||||
|
export const AfterDeleteTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||||
|
TriggerFunction({
|
||||||
|
timing: 'after',
|
||||||
|
actions: ['delete'],
|
||||||
|
...options,
|
||||||
|
});
|
@ -0,0 +1,8 @@
|
|||||||
|
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||||
|
|
||||||
|
export const BeforeUpdateTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||||
|
TriggerFunction({
|
||||||
|
timing: 'before',
|
||||||
|
actions: ['update'],
|
||||||
|
...options,
|
||||||
|
});
|
11
server/src/sql-tools/from-code/decorators/check.decorator.ts
Normal file
11
server/src/sql-tools/from-code/decorators/check.decorator.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
|
||||||
|
export type CheckOptions = {
|
||||||
|
name?: string;
|
||||||
|
expression: string;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
export const Check = (options: CheckOptions): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => void register({ type: 'checkConstraint', item: { object, options } });
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { asOptions } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export type ColumnIndexOptions = {
|
||||||
|
name?: string;
|
||||||
|
unique?: boolean;
|
||||||
|
expression?: string;
|
||||||
|
using?: string;
|
||||||
|
with?: string;
|
||||||
|
where?: string;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
export const ColumnIndex = (options: string | ColumnIndexOptions = {}): PropertyDecorator => {
|
||||||
|
return (object: object, propertyName: string | symbol) =>
|
||||||
|
void register({ type: 'columnIndex', item: { object, propertyName, options: asOptions(options) } });
|
||||||
|
};
|
@ -0,0 +1,30 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { asOptions } from 'src/sql-tools/helpers';
|
||||||
|
import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export type ColumnValue = null | boolean | string | number | object | Date | (() => string);
|
||||||
|
|
||||||
|
export type ColumnBaseOptions = {
|
||||||
|
name?: string;
|
||||||
|
primary?: boolean;
|
||||||
|
type?: ColumnType;
|
||||||
|
nullable?: boolean;
|
||||||
|
length?: number;
|
||||||
|
default?: ColumnValue;
|
||||||
|
comment?: string;
|
||||||
|
synchronize?: boolean;
|
||||||
|
storage?: ColumnStorage;
|
||||||
|
identity?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ColumnOptions = ColumnBaseOptions & {
|
||||||
|
enum?: DatabaseEnum;
|
||||||
|
array?: boolean;
|
||||||
|
unique?: boolean;
|
||||||
|
uniqueConstraintName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => {
|
||||||
|
return (object: object, propertyName: string | symbol) =>
|
||||||
|
void register({ type: 'column', item: { object, propertyName, options: asOptions(options) } });
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { ParameterScope } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export type ConfigurationParameterOptions = {
|
||||||
|
name: string;
|
||||||
|
value: ColumnValue;
|
||||||
|
scope: ParameterScope;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
export const ConfigurationParameter = (options: ConfigurationParameterOptions): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => void register({ type: 'configurationParameter', item: { object, options } });
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
|
||||||
|
export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||||
|
return Column({
|
||||||
|
type: 'timestamp with time zone',
|
||||||
|
default: () => 'now()',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,10 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
|
||||||
|
export type DatabaseOptions = {
|
||||||
|
name?: string;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
export const Database = (options: DatabaseOptions): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => void register({ type: 'database', item: { object, options } });
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
|
||||||
|
export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||||
|
return Column({
|
||||||
|
type: 'timestamp with time zone',
|
||||||
|
nullable: true,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { asOptions } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export type ExtensionOptions = {
|
||||||
|
name: string;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
export const Extension = (options: string | ExtensionOptions): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => void register({ type: 'extension', item: { object, options: asOptions(options) } });
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { asOptions } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export type ExtensionsOptions = {
|
||||||
|
name: string;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
export const Extensions = (options: Array<string | ExtensionsOptions>): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => {
|
||||||
|
for (const option of options) {
|
||||||
|
register({ type: 'extension', item: { object, options: asOptions(option) } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
import { ColumnBaseOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
|
||||||
|
type Action = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
||||||
|
|
||||||
|
export type ForeignKeyColumnOptions = ColumnBaseOptions & {
|
||||||
|
onUpdate?: Action;
|
||||||
|
onDelete?: Action;
|
||||||
|
constraintName?: string;
|
||||||
|
unique?: boolean;
|
||||||
|
uniqueConstraintName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ForeignKeyColumn = (target: () => object, options: ForeignKeyColumnOptions): PropertyDecorator => {
|
||||||
|
return (object: object, propertyName: string | symbol) => {
|
||||||
|
register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } });
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Column, ColumnOptions, ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
import { ColumnType } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export type GeneratedColumnStrategy = 'uuid' | 'identity';
|
||||||
|
|
||||||
|
export type GenerateColumnOptions = Omit<ColumnOptions, 'type'> & {
|
||||||
|
strategy?: GeneratedColumnStrategy;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GeneratedColumn = ({ strategy = 'uuid', ...options }: GenerateColumnOptions): PropertyDecorator => {
|
||||||
|
let columnType: ColumnType | undefined;
|
||||||
|
let columnDefault: ColumnValue | undefined;
|
||||||
|
|
||||||
|
switch (strategy) {
|
||||||
|
case 'uuid': {
|
||||||
|
columnType = 'uuid';
|
||||||
|
columnDefault = () => 'uuid_generate_v4()';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'identity': {
|
||||||
|
columnType = 'integer';
|
||||||
|
options.identity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unsupported strategy for @GeneratedColumn ${strategy}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column({
|
||||||
|
type: columnType,
|
||||||
|
default: columnDefault,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
12
server/src/sql-tools/from-code/decorators/index.decorator.ts
Normal file
12
server/src/sql-tools/from-code/decorators/index.decorator.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ColumnIndexOptions } from 'src/sql-tools/from-code/decorators/column-index.decorator';
|
||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { asOptions } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export type IndexOptions = ColumnIndexOptions & {
|
||||||
|
columns?: string[];
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
export const Index = (options: string | IndexOptions = {}): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => void register({ type: 'index', item: { object, options: asOptions(options) } });
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
|
||||||
|
export const PrimaryColumn = (options: Omit<ColumnOptions, 'primary'> = {}) => Column({ ...options, primary: true });
|
@ -0,0 +1,4 @@
|
|||||||
|
import { GenerateColumnOptions, GeneratedColumn } from 'src/sql-tools/from-code/decorators/generated-column.decorator';
|
||||||
|
|
||||||
|
export const PrimaryGeneratedColumn = (options: Omit<GenerateColumnOptions, 'primary'> = {}) =>
|
||||||
|
GeneratedColumn({ ...options, primary: true });
|
14
server/src/sql-tools/from-code/decorators/table.decorator.ts
Normal file
14
server/src/sql-tools/from-code/decorators/table.decorator.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { asOptions } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export type TableOptions = {
|
||||||
|
name?: string;
|
||||||
|
primaryConstraintName?: string;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Table comments here */
|
||||||
|
export const Table = (options: string | TableOptions = {}): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => void register({ type: 'table', item: { object, options: asOptions(options) } });
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { Trigger, TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
||||||
|
import { DatabaseFunction } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export type TriggerFunctionOptions = Omit<TriggerOptions, 'functionName'> & { function: DatabaseFunction };
|
||||||
|
export const TriggerFunction = (options: TriggerFunctionOptions) =>
|
||||||
|
Trigger({ ...options, functionName: options.function.name });
|
@ -0,0 +1,19 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export type TriggerOptions = {
|
||||||
|
name?: string;
|
||||||
|
timing: TriggerTiming;
|
||||||
|
actions: TriggerAction[];
|
||||||
|
scope: TriggerScope;
|
||||||
|
functionName: string;
|
||||||
|
referencingNewTableAs?: string;
|
||||||
|
referencingOldTableAs?: string;
|
||||||
|
when?: string;
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Trigger = (options: TriggerOptions): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => void register({ type: 'trigger', item: { object, options } });
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
|
||||||
|
export type UniqueOptions = {
|
||||||
|
name?: string;
|
||||||
|
columns: string[];
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
export const Unique = (options: UniqueOptions): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
return (object: Function) => void register({ type: 'uniqueConstraint', item: { object, options } });
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
|
||||||
|
export const UpdateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||||
|
return Column({
|
||||||
|
type: 'timestamp with time zone',
|
||||||
|
default: () => 'now()',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
@ -1,16 +1,21 @@
|
|||||||
import { readdirSync } from 'node:fs';
|
import { readdirSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { reset, schemaFromDecorators } from 'src/sql-tools/schema-from-decorators';
|
import { reset, schemaFromCode } from 'src/sql-tools/from-code';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('schemaDiff', () => {
|
describe(schemaFromCode.name, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
reset();
|
reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(schemaFromDecorators()).toEqual({
|
expect(schemaFromCode()).toEqual({
|
||||||
name: 'public',
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
tables: [],
|
tables: [],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
});
|
});
|
||||||
@ -24,7 +29,7 @@ describe('schemaDiff', () => {
|
|||||||
const module = await import(filePath);
|
const module = await import(filePath);
|
||||||
expect(module.description).toBeDefined();
|
expect(module.description).toBeDefined();
|
||||||
expect(module.schema).toBeDefined();
|
expect(module.schema).toBeDefined();
|
||||||
expect(schemaFromDecorators(), module.description).toEqual(module.schema);
|
expect(schemaFromCode(), module.description).toEqual(module.schema);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
69
server/src/sql-tools/from-code/index.ts
Normal file
69
server/src/sql-tools/from-code/index.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { processCheckConstraints } from 'src/sql-tools/from-code/processors/check-constraint.processor';
|
||||||
|
import { processColumnIndexes } from 'src/sql-tools/from-code/processors/column-index.processor';
|
||||||
|
import { processColumns } from 'src/sql-tools/from-code/processors/column.processor';
|
||||||
|
import { processConfigurationParameters } from 'src/sql-tools/from-code/processors/configuration-parameter.processor';
|
||||||
|
import { processDatabases } from 'src/sql-tools/from-code/processors/database.processor';
|
||||||
|
import { processEnums } from 'src/sql-tools/from-code/processors/enum.processor';
|
||||||
|
import { processExtensions } from 'src/sql-tools/from-code/processors/extension.processor';
|
||||||
|
import { processForeignKeyConstraints } from 'src/sql-tools/from-code/processors/foreign-key-constriant.processor';
|
||||||
|
import { processFunctions } from 'src/sql-tools/from-code/processors/function.processor';
|
||||||
|
import { processIndexes } from 'src/sql-tools/from-code/processors/index.processor';
|
||||||
|
import { processPrimaryKeyConstraints } from 'src/sql-tools/from-code/processors/primary-key-contraint.processor';
|
||||||
|
import { processTables } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { processTriggers } from 'src/sql-tools/from-code/processors/trigger.processor';
|
||||||
|
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { processUniqueConstraints } from 'src/sql-tools/from-code/processors/unique-constraint.processor';
|
||||||
|
import { getRegisteredItems, resetRegisteredItems } from 'src/sql-tools/from-code/register';
|
||||||
|
import { DatabaseSchema } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
let schema: DatabaseSchema;
|
||||||
|
|
||||||
|
export const reset = () => {
|
||||||
|
initialized = false;
|
||||||
|
resetRegisteredItems();
|
||||||
|
};
|
||||||
|
|
||||||
|
const processors: Processor[] = [
|
||||||
|
processDatabases,
|
||||||
|
processConfigurationParameters,
|
||||||
|
processEnums,
|
||||||
|
processExtensions,
|
||||||
|
processFunctions,
|
||||||
|
processTables,
|
||||||
|
processColumns,
|
||||||
|
processUniqueConstraints,
|
||||||
|
processCheckConstraints,
|
||||||
|
processPrimaryKeyConstraints,
|
||||||
|
processIndexes,
|
||||||
|
processColumnIndexes,
|
||||||
|
processForeignKeyConstraints,
|
||||||
|
processTriggers,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const schemaFromCode = () => {
|
||||||
|
if (!initialized) {
|
||||||
|
const builder: SchemaBuilder = {
|
||||||
|
name: 'postgres',
|
||||||
|
schemaName: 'public',
|
||||||
|
tables: [],
|
||||||
|
functions: [],
|
||||||
|
enums: [],
|
||||||
|
extensions: [],
|
||||||
|
parameters: [],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = getRegisteredItems();
|
||||||
|
|
||||||
|
for (const processor of processors) {
|
||||||
|
processor(builder, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
schema = { ...builder, tables: builder.tables.map(({ metadata: _, ...table }) => table) };
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asCheckConstraintName } from 'src/sql-tools/helpers';
|
||||||
|
import { DatabaseConstraintType } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const processCheckConstraints: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { object, options },
|
||||||
|
} of items.filter((item) => item.type === 'checkConstraint')) {
|
||||||
|
const table = resolveTable(builder, object);
|
||||||
|
if (!table) {
|
||||||
|
onMissingTable(builder, '@Check', object);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableName = table.name;
|
||||||
|
|
||||||
|
table.constraints.push({
|
||||||
|
type: DatabaseConstraintType.CHECK,
|
||||||
|
name: options.name || asCheckConstraintName(tableName, options.expression),
|
||||||
|
tableName,
|
||||||
|
expression: options.expression,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
|
||||||
|
import { onMissingTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asIndexName } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export const processColumnIndexes: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { object, propertyName, options },
|
||||||
|
} of items.filter((item) => item.type === 'columnIndex')) {
|
||||||
|
const { table, column } = resolveColumn(builder, object, propertyName);
|
||||||
|
if (!table) {
|
||||||
|
onMissingTable(builder, '@ColumnIndex', object);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!column) {
|
||||||
|
onMissingColumn(builder, `@ColumnIndex`, object, propertyName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.indexes.push({
|
||||||
|
name: options.name || asIndexName(table.name, [column.name], options.where),
|
||||||
|
tableName: table.name,
|
||||||
|
unique: options.unique ?? false,
|
||||||
|
expression: options.expression,
|
||||||
|
using: options.using,
|
||||||
|
where: options.where,
|
||||||
|
columnNames: [column.name],
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
103
server/src/sql-tools/from-code/processors/column.processor.ts
Normal file
103
server/src/sql-tools/from-code/processors/column.processor.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asMetadataKey, asUniqueConstraintName, fromColumnValue } from 'src/sql-tools/helpers';
|
||||||
|
import { DatabaseColumn, DatabaseConstraintType } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const processColumns: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
type,
|
||||||
|
item: { object, propertyName, options },
|
||||||
|
} of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) {
|
||||||
|
const table = resolveTable(builder, object.constructor);
|
||||||
|
if (!table) {
|
||||||
|
onMissingTable(builder, type === 'column' ? '@Column' : '@ForeignKeyColumn', object, propertyName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnName = options.name ?? String(propertyName);
|
||||||
|
const existingColumn = table.columns.find((column) => column.name === columnName);
|
||||||
|
if (existingColumn) {
|
||||||
|
// TODO log warnings if column name is not unique
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableName = table.name;
|
||||||
|
|
||||||
|
let defaultValue = fromColumnValue(options.default);
|
||||||
|
let nullable = options.nullable ?? false;
|
||||||
|
|
||||||
|
// map `{ default: null }` to `{ nullable: true }`
|
||||||
|
if (defaultValue === null) {
|
||||||
|
nullable = true;
|
||||||
|
defaultValue = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnum = !!(options as ColumnOptions).enum;
|
||||||
|
|
||||||
|
const column: DatabaseColumn = {
|
||||||
|
name: columnName,
|
||||||
|
tableName,
|
||||||
|
primary: options.primary ?? false,
|
||||||
|
default: defaultValue,
|
||||||
|
nullable,
|
||||||
|
isArray: (options as ColumnOptions).array ?? false,
|
||||||
|
length: options.length,
|
||||||
|
type: isEnum ? 'enum' : options.type || 'character varying',
|
||||||
|
enumName: isEnum ? (options as ColumnOptions).enum!.name : undefined,
|
||||||
|
comment: options.comment,
|
||||||
|
storage: options.storage,
|
||||||
|
identity: options.identity,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
};
|
||||||
|
|
||||||
|
writeMetadata(object, propertyName, { name: column.name, options });
|
||||||
|
|
||||||
|
table.columns.push(column);
|
||||||
|
|
||||||
|
if (type === 'column' && !options.primary && options.unique) {
|
||||||
|
table.constraints.push({
|
||||||
|
type: DatabaseConstraintType.UNIQUE,
|
||||||
|
name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]),
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames: [column.name],
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type ColumnMetadata = { name: string; options: ColumnOptions };
|
||||||
|
|
||||||
|
export const resolveColumn = (builder: SchemaBuilder, object: object, propertyName: string | symbol) => {
|
||||||
|
const table = resolveTable(builder, object.constructor);
|
||||||
|
if (!table) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = readMetadata(object, propertyName);
|
||||||
|
if (!metadata) {
|
||||||
|
return { table };
|
||||||
|
}
|
||||||
|
|
||||||
|
const column = table.columns.find((column) => column.name === metadata.name);
|
||||||
|
return { table, column };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onMissingColumn = (
|
||||||
|
builder: SchemaBuilder,
|
||||||
|
context: string,
|
||||||
|
object: object,
|
||||||
|
propertyName?: symbol | string,
|
||||||
|
) => {
|
||||||
|
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||||
|
builder.warnings.push(`[${context}] Unable to find column (${label})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const METADATA_KEY = asMetadataKey('table-metadata');
|
||||||
|
|
||||||
|
const writeMetadata = (object: object, propertyName: symbol | string, metadata: ColumnMetadata) =>
|
||||||
|
Reflect.defineMetadata(METADATA_KEY, metadata, object, propertyName);
|
||||||
|
|
||||||
|
const readMetadata = (object: object, propertyName: symbol | string): ColumnMetadata | undefined =>
|
||||||
|
Reflect.getMetadata(METADATA_KEY, object, propertyName);
|
@ -0,0 +1,16 @@
|
|||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { fromColumnValue } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export const processConfigurationParameters: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { options },
|
||||||
|
} of items.filter((item) => item.type === 'configurationParameter')) {
|
||||||
|
builder.parameters.push({
|
||||||
|
databaseName: builder.name,
|
||||||
|
name: options.name,
|
||||||
|
value: fromColumnValue(options.value),
|
||||||
|
scope: options.scope,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,10 @@
|
|||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asSnakeCase } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export const processDatabases: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { object, options },
|
||||||
|
} of items.filter((item) => item.type === 'database')) {
|
||||||
|
builder.name = options.name || asSnakeCase(object.name);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,8 @@
|
|||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
|
||||||
|
export const processEnums: Processor = (builder, items) => {
|
||||||
|
for (const { item } of items.filter((item) => item.type === 'enum')) {
|
||||||
|
// TODO log warnings if enum name is not unique
|
||||||
|
builder.enums.push(item);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
|
||||||
|
export const processExtensions: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { options },
|
||||||
|
} of items.filter((item) => item.type === 'extension')) {
|
||||||
|
builder.extensions.push({
|
||||||
|
name: options.name,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,59 @@
|
|||||||
|
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
|
||||||
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asForeignKeyConstraintName, asRelationKeyConstraintName } from 'src/sql-tools/helpers';
|
||||||
|
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const processForeignKeyConstraints: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { object, propertyName, options, target },
|
||||||
|
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
|
||||||
|
const { table, column } = resolveColumn(builder, object, propertyName);
|
||||||
|
if (!table) {
|
||||||
|
onMissingTable(builder, '@ForeignKeyColumn', object);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!column) {
|
||||||
|
// should be impossible since they are pre-created in `column.processor.ts`
|
||||||
|
onMissingColumn(builder, '@ForeignKeyColumn', object, propertyName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const referenceTable = resolveTable(builder, target());
|
||||||
|
if (!referenceTable) {
|
||||||
|
onMissingTable(builder, '@ForeignKeyColumn', object, propertyName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnNames = [column.name];
|
||||||
|
const referenceColumns = referenceTable.columns.filter((column) => column.primary);
|
||||||
|
|
||||||
|
// infer FK column type from reference table
|
||||||
|
if (referenceColumns.length === 1) {
|
||||||
|
column.type = referenceColumns[0].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.constraints.push({
|
||||||
|
name: options.constraintName || asForeignKeyConstraintName(table.name, columnNames),
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames,
|
||||||
|
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||||
|
referenceTableName: referenceTable.name,
|
||||||
|
referenceColumnNames: referenceColumns.map((column) => column.name),
|
||||||
|
onUpdate: options.onUpdate as DatabaseActionType,
|
||||||
|
onDelete: options.onDelete as DatabaseActionType,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.unique) {
|
||||||
|
table.constraints.push({
|
||||||
|
name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames),
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames,
|
||||||
|
type: DatabaseConstraintType.UNIQUE,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,8 @@
|
|||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
|
||||||
|
export const processFunctions: Processor = (builder, items) => {
|
||||||
|
for (const { item } of items.filter((item) => item.type === 'function')) {
|
||||||
|
// TODO log warnings if function name is not unique
|
||||||
|
builder.functions.push(item);
|
||||||
|
}
|
||||||
|
};
|
27
server/src/sql-tools/from-code/processors/index.processor.ts
Normal file
27
server/src/sql-tools/from-code/processors/index.processor.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asIndexName } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export const processIndexes: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { object, options },
|
||||||
|
} of items.filter((item) => item.type === 'index')) {
|
||||||
|
const table = resolveTable(builder, object);
|
||||||
|
if (!table) {
|
||||||
|
onMissingTable(builder, '@Check', object);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.indexes.push({
|
||||||
|
name: options.name || asIndexName(table.name, options.columns, options.where),
|
||||||
|
tableName: table.name,
|
||||||
|
unique: options.unique ?? false,
|
||||||
|
expression: options.expression,
|
||||||
|
using: options.using,
|
||||||
|
with: options.with,
|
||||||
|
where: options.where,
|
||||||
|
columnNames: options.columns,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asPrimaryKeyConstraintName } from 'src/sql-tools/helpers';
|
||||||
|
import { DatabaseConstraintType } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const processPrimaryKeyConstraints: Processor = (builder) => {
|
||||||
|
for (const table of builder.tables) {
|
||||||
|
const columnNames: string[] = [];
|
||||||
|
|
||||||
|
for (const column of table.columns) {
|
||||||
|
if (column.primary) {
|
||||||
|
columnNames.push(column.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (columnNames.length > 0) {
|
||||||
|
table.constraints.push({
|
||||||
|
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||||
|
name: table.metadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames),
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames,
|
||||||
|
synchronize: table.metadata.options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
51
server/src/sql-tools/from-code/processors/table.processor.ts
Normal file
51
server/src/sql-tools/from-code/processors/table.processor.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||||
|
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asMetadataKey, asSnakeCase } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export const processTables: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { options, object },
|
||||||
|
} of items.filter((item) => item.type === 'table')) {
|
||||||
|
const tableName = options.name || asSnakeCase(object.name);
|
||||||
|
|
||||||
|
writeMetadata(object, { name: tableName, options });
|
||||||
|
|
||||||
|
builder.tables.push({
|
||||||
|
name: tableName,
|
||||||
|
columns: [],
|
||||||
|
constraints: [],
|
||||||
|
indexes: [],
|
||||||
|
triggers: [],
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
metadata: { options, object },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveTable = (builder: SchemaBuilder, object: object) => {
|
||||||
|
const metadata = readMetadata(object);
|
||||||
|
if (!metadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.tables.find((table) => table.name === metadata.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onMissingTable = (
|
||||||
|
builder: SchemaBuilder,
|
||||||
|
context: string,
|
||||||
|
object: object,
|
||||||
|
propertyName?: symbol | string,
|
||||||
|
) => {
|
||||||
|
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||||
|
builder.warnings.push(`[${context}] Unable to find table (${label})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const METADATA_KEY = asMetadataKey('table-metadata');
|
||||||
|
|
||||||
|
type TableMetadata = { name: string; options: TableOptions };
|
||||||
|
|
||||||
|
const readMetadata = (object: object): TableMetadata | undefined => Reflect.getMetadata(METADATA_KEY, object);
|
||||||
|
|
||||||
|
const writeMetadata = (object: object, metadata: TableMetadata): void =>
|
||||||
|
Reflect.defineMetadata(METADATA_KEY, metadata, object);
|
@ -0,0 +1,28 @@
|
|||||||
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asTriggerName } from 'src/sql-tools/helpers';
|
||||||
|
|
||||||
|
export const processTriggers: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { object, options },
|
||||||
|
} of items.filter((item) => item.type === 'trigger')) {
|
||||||
|
const table = resolveTable(builder, object);
|
||||||
|
if (!table) {
|
||||||
|
onMissingTable(builder, '@Trigger', object);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.triggers.push({
|
||||||
|
name: options.name || asTriggerName(table.name, options),
|
||||||
|
tableName: table.name,
|
||||||
|
timing: options.timing,
|
||||||
|
actions: options.actions,
|
||||||
|
when: options.when,
|
||||||
|
scope: options.scope,
|
||||||
|
referencingNewTableAs: options.referencingNewTableAs,
|
||||||
|
referencingOldTableAs: options.referencingOldTableAs,
|
||||||
|
functionName: options.functionName,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
9
server/src/sql-tools/from-code/processors/type.ts
Normal file
9
server/src/sql-tools/from-code/processors/type.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||||
|
import { RegisterItem } from 'src/sql-tools/from-code/register-item';
|
||||||
|
import { DatabaseSchema, DatabaseTable } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
export type TableWithMetadata = DatabaseTable & { metadata: { options: TableOptions; object: Function } };
|
||||||
|
export type SchemaBuilder = Omit<DatabaseSchema, 'tables'> & { tables: TableWithMetadata[] };
|
||||||
|
|
||||||
|
export type Processor = (builder: SchemaBuilder, items: RegisterItem[]) => void;
|
@ -0,0 +1,27 @@
|
|||||||
|
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||||
|
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||||
|
import { asUniqueConstraintName } from 'src/sql-tools/helpers';
|
||||||
|
import { DatabaseConstraintType } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export const processUniqueConstraints: Processor = (builder, items) => {
|
||||||
|
for (const {
|
||||||
|
item: { object, options },
|
||||||
|
} of items.filter((item) => item.type === 'uniqueConstraint')) {
|
||||||
|
const table = resolveTable(builder, object);
|
||||||
|
if (!table) {
|
||||||
|
onMissingTable(builder, '@Unique', object);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableName = table.name;
|
||||||
|
const columnNames = options.columns;
|
||||||
|
|
||||||
|
table.constraints.push({
|
||||||
|
type: DatabaseConstraintType.UNIQUE,
|
||||||
|
name: options.name || asUniqueConstraintName(tableName, columnNames),
|
||||||
|
tableName,
|
||||||
|
columnNames,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
20
server/src/sql-tools/from-code/register-enum.ts
Normal file
20
server/src/sql-tools/from-code/register-enum.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { DatabaseEnum } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export type EnumOptions = {
|
||||||
|
name: string;
|
||||||
|
values: string[];
|
||||||
|
synchronize?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerEnum = (options: EnumOptions) => {
|
||||||
|
const item: DatabaseEnum = {
|
||||||
|
name: options.name,
|
||||||
|
values: options.values,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
};
|
||||||
|
|
||||||
|
register({ type: 'enum', item });
|
||||||
|
|
||||||
|
return item;
|
||||||
|
};
|
29
server/src/sql-tools/from-code/register-function.ts
Normal file
29
server/src/sql-tools/from-code/register-function.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { register } from 'src/sql-tools/from-code/register';
|
||||||
|
import { asFunctionExpression } from 'src/sql-tools/helpers';
|
||||||
|
import { ColumnType, DatabaseFunction } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export type FunctionOptions = {
|
||||||
|
name: string;
|
||||||
|
arguments?: string[];
|
||||||
|
returnType: ColumnType | string;
|
||||||
|
language?: 'SQL' | 'PLPGSQL';
|
||||||
|
behavior?: 'immutable' | 'stable' | 'volatile';
|
||||||
|
parallel?: 'safe' | 'unsafe' | 'restricted';
|
||||||
|
strict?: boolean;
|
||||||
|
synchronize?: boolean;
|
||||||
|
} & ({ body: string } | { return: string });
|
||||||
|
|
||||||
|
export const registerFunction = (options: FunctionOptions) => {
|
||||||
|
const name = options.name;
|
||||||
|
const expression = asFunctionExpression(options);
|
||||||
|
|
||||||
|
const item: DatabaseFunction = {
|
||||||
|
name,
|
||||||
|
expression,
|
||||||
|
synchronize: options.synchronize ?? true,
|
||||||
|
};
|
||||||
|
|
||||||
|
register({ type: 'function', item });
|
||||||
|
|
||||||
|
return item;
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user