mirror of
https://github.com/immich-app/immich.git
synced 2026-02-02 02:04:47 -08:00
Compare commits
4 Commits
v1.135.0
...
fix/map-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c7a29b2f4 | ||
|
|
0a9a520ed2 | ||
|
|
de81006367 | ||
|
|
e0144b4ece |
3
mobile/openapi/lib/model/sync_entity_type.dart
generated
3
mobile/openapi/lib/model/sync_entity_type.dart
generated
@@ -38,6 +38,7 @@ class SyncEntityType {
|
||||
static const albumV1 = SyncEntityType._(r'AlbumV1');
|
||||
static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1');
|
||||
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
||||
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
||||
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
||||
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
||||
|
||||
@@ -58,6 +59,7 @@ class SyncEntityType {
|
||||
albumV1,
|
||||
albumDeleteV1,
|
||||
albumUserV1,
|
||||
albumUserBackfillV1,
|
||||
albumUserDeleteV1,
|
||||
syncAckV1,
|
||||
];
|
||||
@@ -113,6 +115,7 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'AlbumV1': return SyncEntityType.albumV1;
|
||||
case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1;
|
||||
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
||||
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
||||
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
||||
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
||||
default:
|
||||
|
||||
@@ -13706,6 +13706,7 @@
|
||||
"AlbumV1",
|
||||
"AlbumDeleteV1",
|
||||
"AlbumUserV1",
|
||||
"AlbumUserBackfillV1",
|
||||
"AlbumUserDeleteV1",
|
||||
"SyncAckV1"
|
||||
],
|
||||
|
||||
@@ -4059,6 +4059,7 @@ export enum SyncEntityType {
|
||||
AlbumV1 = "AlbumV1",
|
||||
AlbumDeleteV1 = "AlbumDeleteV1",
|
||||
AlbumUserV1 = "AlbumUserV1",
|
||||
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
||||
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
||||
SyncAckV1 = "SyncAckV1"
|
||||
}
|
||||
|
||||
@@ -355,6 +355,12 @@ export const columns = {
|
||||
'updateId',
|
||||
'duration',
|
||||
],
|
||||
syncAlbumUser: [
|
||||
'albums_shared_users_users.albumsId as albumId',
|
||||
'albums_shared_users_users.usersId as userId',
|
||||
'albums_shared_users_users.role',
|
||||
'albums_shared_users_users.updateId',
|
||||
],
|
||||
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
||||
syncAssetExif: [
|
||||
'exif.assetId',
|
||||
|
||||
4
server/src/db.d.ts
vendored
4
server/src/db.d.ts
vendored
@@ -98,8 +98,10 @@ export interface AlbumsSharedUsersUsers {
|
||||
albumsId: string;
|
||||
role: Generated<AlbumUserRole>;
|
||||
usersId: string;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
createId: Generated<string>;
|
||||
createdAt: Generated<Timestamp>;
|
||||
updateId: Generated<string>;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
}
|
||||
|
||||
export interface ApiKeys {
|
||||
|
||||
@@ -161,6 +161,7 @@ export type SyncItem = {
|
||||
[SyncEntityType.AlbumV1]: SyncAlbumV1;
|
||||
[SyncEntityType.AlbumDeleteV1]: SyncAlbumDeleteV1;
|
||||
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||
[SyncEntityType.SyncAckV1]: object;
|
||||
};
|
||||
|
||||
@@ -603,6 +603,7 @@ export enum SyncEntityType {
|
||||
AlbumV1 = 'AlbumV1',
|
||||
AlbumDeleteV1 = 'AlbumDeleteV1',
|
||||
AlbumUserV1 = 'AlbumUserV1',
|
||||
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
||||
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||
|
||||
SyncAckV1 = 'SyncAckV1',
|
||||
|
||||
@@ -394,6 +394,35 @@ where
|
||||
order by
|
||||
"id" asc
|
||||
|
||||
-- SyncRepository.getAlbumBackfill
|
||||
select
|
||||
"albumsId" as "id",
|
||||
"createId"
|
||||
from
|
||||
"albums_shared_users_users"
|
||||
where
|
||||
"usersId" = $1
|
||||
and "createId" >= $2
|
||||
and "createdAt" < now() - interval '1 millisecond'
|
||||
order by
|
||||
"createId" asc
|
||||
|
||||
-- SyncRepository.getAlbumUsersBackfill
|
||||
select
|
||||
"albums_shared_users_users"."albumsId" as "albumId",
|
||||
"albums_shared_users_users"."usersId" as "userId",
|
||||
"albums_shared_users_users"."role",
|
||||
"albums_shared_users_users"."updateId"
|
||||
from
|
||||
"albums_shared_users_users"
|
||||
where
|
||||
"albumsId" = $1
|
||||
and "updatedAt" < now() - interval '1 millisecond'
|
||||
and "updateId" < $2
|
||||
and "updateId" >= $3
|
||||
order by
|
||||
"updateId" asc
|
||||
|
||||
-- SyncRepository.getAlbumUserUpserts
|
||||
select
|
||||
"albums_shared_users_users"."albumsId" as "albumId",
|
||||
|
||||
@@ -254,16 +254,36 @@ export class SyncRepository {
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
getAlbumBackfill(userId: string, afterCreateId?: string) {
|
||||
return this.db
|
||||
.selectFrom('albums_shared_users_users')
|
||||
.select(['albumsId as id', 'createId'])
|
||||
.where('usersId', '=', userId)
|
||||
.$if(!!afterCreateId, (qb) => qb.where('createId', '>=', afterCreateId!))
|
||||
.where('createdAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.orderBy('createId', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||
getAlbumUsersBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||
return this.db
|
||||
.selectFrom('albums_shared_users_users')
|
||||
.select(columns.syncAlbumUser)
|
||||
.where('albumsId', '=', albumId)
|
||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.where('updateId', '<', beforeUpdateId)
|
||||
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
|
||||
.orderBy('updateId', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||
getAlbumUserUpserts(userId: string, ack?: SyncAck) {
|
||||
return this.db
|
||||
.selectFrom('albums_shared_users_users')
|
||||
.select([
|
||||
'albums_shared_users_users.albumsId as albumId',
|
||||
'albums_shared_users_users.usersId as userId',
|
||||
'albums_shared_users_users.role',
|
||||
'albums_shared_users_users.updateId',
|
||||
])
|
||||
.select(columns.syncAlbumUser)
|
||||
.where('albums_shared_users_users.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||
.$if(!!ack, (qb) => qb.where('albums_shared_users_users.updateId', '>', ack!.updateId))
|
||||
.orderBy('albums_shared_users_users.updateId', 'asc')
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "albums_shared_users_users" ADD "createId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||
await sql`ALTER TABLE "albums_shared_users_users" ADD "createdAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_users_create_id" ON "albums_shared_users_users" ("createId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_partners_create_id" ON "partners" ("createId")`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP INDEX "IDX_partners_create_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_album_users_create_id";`.execute(db);
|
||||
await sql`ALTER TABLE "albums_shared_users_users" DROP COLUMN "createId";`.execute(db);
|
||||
await sql`ALTER TABLE "albums_shared_users_users" DROP COLUMN "createdAt";`.execute(db);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { album_user_after_insert, album_users_delete_audit } from 'src/schema/functions';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AfterDeleteTrigger,
|
||||
AfterInsertTrigger,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Index,
|
||||
Table,
|
||||
@@ -51,6 +52,12 @@ export class AlbumUserTable {
|
||||
@Column({ type: 'character varying', default: AlbumUserRole.EDITOR })
|
||||
role!: AlbumUserRole;
|
||||
|
||||
@CreateIdColumn({ indexName: 'IDX_album_users_create_id' })
|
||||
createId?: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateIdColumn({ indexName: 'IDX_album_users_update_id' })
|
||||
updateId?: string;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export class PartnerTable {
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@CreateIdColumn()
|
||||
@CreateIdColumn({ indexName: 'IDX_partners_create_id' })
|
||||
createId!: string;
|
||||
|
||||
@UpdateDateColumn()
|
||||
|
||||
@@ -138,14 +138,14 @@ export class SyncService extends BaseService {
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.PartnerAssetsV1: {
|
||||
await this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId);
|
||||
|
||||
case SyncRequestType.AssetExifsV1: {
|
||||
await this.syncAssetExifsV1(response, checkpointMap, auth);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncRequestType.AssetExifsV1: {
|
||||
await this.syncAssetExifsV1(response, checkpointMap, auth);
|
||||
case SyncRequestType.PartnerAssetsV1: {
|
||||
await this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
case SyncRequestType.AlbumUsersV1: {
|
||||
await this.syncAlbumUsersV1(response, checkpointMap, auth);
|
||||
await this.syncAlbumUsersV1(response, checkpointMap, auth, sessionId);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -330,18 +330,50 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumUsersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto) {
|
||||
const deletes = this.syncRepository.getAlbumUserDeletes(
|
||||
auth.user.id,
|
||||
checkpointMap[SyncEntityType.AlbumUserDeleteV1],
|
||||
);
|
||||
private async syncAlbumUsersV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
||||
const backfillType = SyncEntityType.AlbumUserBackfillV1;
|
||||
const upsertType = SyncEntityType.AlbumUserV1;
|
||||
const deleteType = SyncEntityType.AlbumUserDeleteV1;
|
||||
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
|
||||
const deletes = this.syncRepository.getAlbumUserDeletes(auth.user.id, checkpointMap[deleteType]);
|
||||
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: SyncEntityType.AlbumUserDeleteV1, ids: [id], data });
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getAlbumUserUpserts(auth.user.id, checkpointMap[SyncEntityType.AlbumUserV1]);
|
||||
const albums = await this.syncRepository.getAlbumBackfill(auth.user.id, backfillCheckpoint?.updateId);
|
||||
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
|
||||
for (const album of albums) {
|
||||
if (isEntityBackfillComplete(album, backfillCheckpoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startId = getStartId(album, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.getAlbumUsersBackfill(album.id, startId, endId);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [updateId], data });
|
||||
}
|
||||
|
||||
sendEntityBackfillCompleteAck(response, backfillType, album.id);
|
||||
}
|
||||
} else if (albums.length > 0) {
|
||||
await this.upsertBackfillCheckpoint({
|
||||
type: backfillType,
|
||||
sessionId,
|
||||
createId: albums.at(-1)!.createId,
|
||||
});
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.getAlbumUserUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: SyncEntityType.AlbumUserV1, ids: [updateId], data });
|
||||
send(response, { type: upsertType, ids: [updateId], data });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ColumnBaseOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { ForeignKeyAction } from 'src/sql-tools/from-code/decorators/foreign-key-constraint.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;
|
||||
onUpdate?: ForeignKeyAction;
|
||||
onDelete?: ForeignKeyAction;
|
||||
constraintName?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
|
||||
export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
||||
|
||||
export type ForeignKeyConstraintOptions = {
|
||||
name?: string;
|
||||
index?: boolean;
|
||||
indexName?: string;
|
||||
columns: string[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
referenceTable: () => Function;
|
||||
referenceColumns?: string[];
|
||||
onUpdate?: ForeignKeyAction;
|
||||
onDelete?: ForeignKeyAction;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
|
||||
export const ForeignKeyConstraint = (options: ForeignKeyConstraintOptions): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (target: Function) => {
|
||||
register({ type: 'foreignKeyConstraint', item: { object: target, options } });
|
||||
};
|
||||
};
|
||||
@@ -5,7 +5,8 @@ import { processConfigurationParameters } from 'src/sql-tools/from-code/processo
|
||||
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 { processForeignKeyColumns } from 'src/sql-tools/from-code/processors/foreign-key-column.processor';
|
||||
import { processForeignKeyConstraints } from 'src/sql-tools/from-code/processors/foreign-key-constraint.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';
|
||||
@@ -32,10 +33,11 @@ const processors: Processor[] = [
|
||||
processFunctions,
|
||||
processTables,
|
||||
processColumns,
|
||||
processForeignKeyColumns,
|
||||
processForeignKeyConstraints,
|
||||
processUniqueConstraints,
|
||||
processCheckConstraints,
|
||||
processPrimaryKeyConstraints,
|
||||
processForeignKeyConstraints,
|
||||
processIndexes,
|
||||
processTriggers,
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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, fromColumnValue } from 'src/sql-tools/helpers';
|
||||
import { addWarning, asMetadataKey, fromColumnValue } from 'src/sql-tools/helpers';
|
||||
import { DatabaseColumn } from 'src/sql-tools/types';
|
||||
|
||||
export const processColumns: Processor = (builder, items) => {
|
||||
@@ -81,7 +81,7 @@ export const onMissingColumn = (
|
||||
propertyName?: symbol | string,
|
||||
) => {
|
||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||
builder.warnings.push(`[${context}] Unable to find column (${label})`);
|
||||
addWarning(builder, context, `Unable to find column (${label})`);
|
||||
};
|
||||
|
||||
const METADATA_KEY = asMetadataKey('table-metadata');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 { asKey } from 'src/sql-tools/helpers';
|
||||
import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers';
|
||||
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
|
||||
|
||||
export const processForeignKeyConstraints: Processor = (builder, items) => {
|
||||
export const processForeignKeyColumns: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, propertyName, options, target },
|
||||
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
|
||||
@@ -34,13 +34,16 @@ export const processForeignKeyConstraints: Processor = (builder, items) => {
|
||||
column.type = referenceColumns[0].type;
|
||||
}
|
||||
|
||||
const referenceColumnNames = referenceColumns.map((column) => column.name);
|
||||
const name = options.constraintName || asForeignKeyConstraintName(table.name, columnNames);
|
||||
|
||||
table.constraints.push({
|
||||
name: options.constraintName || asForeignKeyConstraintName(table.name, columnNames),
|
||||
name,
|
||||
tableName: table.name,
|
||||
columnNames,
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
referenceTableName: referenceTable.name,
|
||||
referenceColumnNames: referenceColumns.map((column) => column.name),
|
||||
referenceColumnNames,
|
||||
onUpdate: options.onUpdate as DatabaseActionType,
|
||||
onDelete: options.onDelete as DatabaseActionType,
|
||||
synchronize: options.synchronize ?? true,
|
||||
@@ -58,5 +61,4 @@ export const processForeignKeyConstraints: Processor = (builder, items) => {
|
||||
}
|
||||
};
|
||||
|
||||
const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, columns);
|
||||
const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns);
|
||||
@@ -0,0 +1,86 @@
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { addWarning, asForeignKeyConstraintName, asIndexName } from 'src/sql-tools/helpers';
|
||||
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
|
||||
|
||||
export const processForeignKeyConstraints: Processor = (builder, items, config) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'foreignKeyConstraint')) {
|
||||
const table = resolveTable(builder, object);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@ForeignKeyConstraint', { name: 'referenceTable' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const referenceTable = resolveTable(builder, options.referenceTable());
|
||||
if (!referenceTable) {
|
||||
const referenceTableName = options.referenceTable()?.name;
|
||||
addWarning(
|
||||
builder,
|
||||
'@ForeignKeyConstraint.referenceTable',
|
||||
`Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let missingColumn = false;
|
||||
|
||||
for (const columnName of options.columns) {
|
||||
if (!table.columns.some(({ name }) => name === columnName)) {
|
||||
addWarning(
|
||||
builder,
|
||||
'@ForeignKeyConstraint.columns',
|
||||
`Unable to find column (${table.metadata.object.name}.${columnName})`,
|
||||
);
|
||||
missingColumn = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const columnName of options.referenceColumns || []) {
|
||||
if (!referenceTable.columns.some(({ name }) => name === columnName)) {
|
||||
addWarning(
|
||||
builder,
|
||||
'@ForeignKeyConstraint.referenceColumns',
|
||||
`Unable to find column (${referenceTable.metadata.object.name}.${columnName})`,
|
||||
);
|
||||
missingColumn = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (missingColumn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const referenceColumns =
|
||||
options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name);
|
||||
|
||||
const name = options.name || asForeignKeyConstraintName(table.name, options.columns);
|
||||
|
||||
table.constraints.push({
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
name,
|
||||
tableName: table.name,
|
||||
columnNames: options.columns,
|
||||
referenceTableName: referenceTable.name,
|
||||
referenceColumnNames: referenceColumns,
|
||||
onUpdate: options.onUpdate as DatabaseActionType,
|
||||
onDelete: options.onDelete as DatabaseActionType,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
|
||||
if (options.index === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.index || options.indexName || config.createForeignKeyIndexes) {
|
||||
table.indexes.push({
|
||||
name: options.indexName || asIndexName(table.name, options.columns),
|
||||
tableName: table.name,
|
||||
columnNames: options.columns,
|
||||
unique: false,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { asKey } from 'src/sql-tools/helpers';
|
||||
import { asIndexName } from 'src/sql-tools/helpers';
|
||||
|
||||
export const processIndexes: Processor = (builder, items, config) => {
|
||||
for (const {
|
||||
@@ -75,16 +75,3 @@ export const processIndexes: Processor = (builder, items, config) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const asIndexName = (table: string, columns?: string[], where?: string) => {
|
||||
const items: string[] = [];
|
||||
for (const columnName of columns ?? []) {
|
||||
items.push(columnName);
|
||||
}
|
||||
|
||||
if (where) {
|
||||
items.push(where);
|
||||
}
|
||||
|
||||
return asKey('IDX_', table, items);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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';
|
||||
import { addWarning, asMetadataKey, asSnakeCase } from 'src/sql-tools/helpers';
|
||||
|
||||
export const processTables: Processor = (builder, items) => {
|
||||
for (const {
|
||||
@@ -45,7 +45,7 @@ export const onMissingTable = (
|
||||
propertyName?: symbol | string,
|
||||
) => {
|
||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||
builder.warnings.push(`[${context}] Unable to find table (${label})`);
|
||||
addWarning(builder, context, `Unable to find table (${label})`);
|
||||
};
|
||||
|
||||
const METADATA_KEY = asMetadataKey('table-metadata');
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ConfigurationParameterOptions } from 'src/sql-tools/from-code/decorator
|
||||
import { DatabaseOptions } from 'src/sql-tools/from-code/decorators/database.decorator';
|
||||
import { ExtensionOptions } from 'src/sql-tools/from-code/decorators/extension.decorator';
|
||||
import { ForeignKeyColumnOptions } from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator';
|
||||
import { ForeignKeyConstraintOptions } from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator';
|
||||
import { IndexOptions } from 'src/sql-tools/from-code/decorators/index.decorator';
|
||||
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||
import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
||||
@@ -25,5 +26,6 @@ export type RegisterItem =
|
||||
| { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> }
|
||||
| { type: 'extension'; item: ClassBased<{ options: ExtensionOptions }> }
|
||||
| { type: 'configurationParameter'; item: ClassBased<{ options: ConfigurationParameterOptions }> }
|
||||
| { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }> };
|
||||
| { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }> }
|
||||
| { type: 'foreignKeyConstraint'; item: ClassBased<{ options: ForeignKeyConstraintOptions }> };
|
||||
export type RegisterItemType<T extends RegisterItem['type']> = Extract<RegisterItem, { type: T }>['item'];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||
import {
|
||||
Comparer,
|
||||
DatabaseColumn,
|
||||
@@ -211,3 +212,22 @@ export const asColumnComment = (tableName: string, columnName: string, comment:
|
||||
};
|
||||
|
||||
export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', ');
|
||||
|
||||
export const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, [...columns]);
|
||||
|
||||
export const asIndexName = (table: string, columns?: string[], where?: string) => {
|
||||
const items: string[] = [];
|
||||
for (const columnName of columns ?? []) {
|
||||
items.push(columnName);
|
||||
}
|
||||
|
||||
if (where) {
|
||||
items.push(where);
|
||||
}
|
||||
|
||||
return asKey('IDX_', table, items);
|
||||
};
|
||||
|
||||
export const addWarning = (builder: SchemaBuilder, context: string, message: string) => {
|
||||
builder.warnings.push(`[${context}] ${message}`);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ export * from 'src/sql-tools/from-code/decorators/delete-date-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/extension.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/extensions.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/generated-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/index.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/primary-column.decorator';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
import { getKyselyDB, wait } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
@@ -265,5 +265,95 @@ describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should backfill album users when a user shares an album with you', async () => {
|
||||
const { auth, sut, testSync, getRepository } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
await userRepo.create(user2);
|
||||
|
||||
const album1 = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||
const album2 = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||
await albumRepo.create(album1, [], []);
|
||||
await albumRepo.create(album2, [], []);
|
||||
|
||||
// backfill album user
|
||||
await albumUserRepo.create({ albumsId: album1.id, usersId: user1.id, role: AlbumUserRole.EDITOR });
|
||||
await wait(2);
|
||||
// initial album user
|
||||
await albumUserRepo.create({ albumsId: album2.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
await wait(2);
|
||||
// post checkpoint album user
|
||||
await albumUserRepo.create({ albumsId: album1.id, usersId: user2.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album2.id,
|
||||
role: AlbumUserRole.EDITOR,
|
||||
userId: auth.user.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
|
||||
// ack initial user
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
// get access to the backfill album user
|
||||
await albumUserRepo.create({ albumsId: album1.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
// should backfill the album user
|
||||
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(backfillResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album1.id,
|
||||
role: AlbumUserRole.EDITOR,
|
||||
userId: user1.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserBackfillV1,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.AlbumUserBackfillV1),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album1.id,
|
||||
role: AlbumUserRole.EDITOR,
|
||||
userId: user2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album1.id,
|
||||
role: AlbumUserRole.EDITOR,
|
||||
userId: auth.user.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await sut.setAcks(auth, { acks: [backfillResponse[1].ack, backfillResponse.at(-1).ack] });
|
||||
|
||||
const finalResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(finalResponse).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
Column,
|
||||
DatabaseConstraintType,
|
||||
DatabaseSchema,
|
||||
ForeignKeyConstraint,
|
||||
PrimaryColumn,
|
||||
Table,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table()
|
||||
export class Table1 {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id1!: string;
|
||||
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id2!: string;
|
||||
}
|
||||
|
||||
@Table()
|
||||
@ForeignKeyConstraint({
|
||||
columns: ['parentId1', 'parentId2'],
|
||||
referenceTable: () => Table1,
|
||||
referenceColumns: ['id2', 'id1'],
|
||||
})
|
||||
export class Table2 {
|
||||
@Column({ type: 'uuid' })
|
||||
parentId1!: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
parentId2!: string;
|
||||
}
|
||||
|
||||
export const description = 'should create a foreign key constraint to the target table';
|
||||
export const schema: DatabaseSchema = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
name: 'id1',
|
||||
tableName: 'table1',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: true,
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'id2',
|
||||
tableName: 'table1',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: true,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||
name: 'PK_e457e8b1301b7bc06ef78188ee4',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id1', 'id2'],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'table2',
|
||||
columns: [
|
||||
{
|
||||
name: 'parentId1',
|
||||
tableName: 'table2',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'parentId2',
|
||||
tableName: 'table2',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
name: 'IDX_aed36d04470eba20161aa8b1dc',
|
||||
tableName: 'table2',
|
||||
columnNames: ['parentId1', 'parentId2'],
|
||||
unique: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
name: 'FK_aed36d04470eba20161aa8b1dc6',
|
||||
tableName: 'table2',
|
||||
columnNames: ['parentId1', 'parentId2'],
|
||||
referenceColumnNames: ['id2', 'id1'],
|
||||
referenceTableName: 'table1',
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Column,
|
||||
DatabaseConstraintType,
|
||||
DatabaseSchema,
|
||||
ForeignKeyConstraint,
|
||||
PrimaryColumn,
|
||||
Table,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table()
|
||||
export class Table1 {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
}
|
||||
|
||||
@Table()
|
||||
@ForeignKeyConstraint({ columns: ['parentId2'], referenceTable: () => Table1 })
|
||||
export class Table2 {
|
||||
@Column({ type: 'uuid' })
|
||||
parentId!: string;
|
||||
}
|
||||
|
||||
export const description = 'should warn against missing column in foreign key constraint';
|
||||
export const schema: DatabaseSchema = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
tableName: 'table1',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: true,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||
name: 'PK_b249cc64cf63b8a22557cdc8537',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id'],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'table2',
|
||||
columns: [
|
||||
{
|
||||
name: 'parentId',
|
||||
tableName: 'table2',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
warnings: ['[@ForeignKeyConstraint.columns] Unable to find column (Table2.parentId2)'],
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Column,
|
||||
DatabaseConstraintType,
|
||||
DatabaseSchema,
|
||||
ForeignKeyConstraint,
|
||||
PrimaryColumn,
|
||||
Table,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table()
|
||||
export class Table1 {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
}
|
||||
|
||||
@Table()
|
||||
@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1, referenceColumns: ['foo'] })
|
||||
export class Table2 {
|
||||
@Column({ type: 'uuid' })
|
||||
parentId!: string;
|
||||
}
|
||||
|
||||
export const description = 'should warn against missing reference column in foreign key constraint';
|
||||
export const schema: DatabaseSchema = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
tableName: 'table1',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: true,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||
name: 'PK_b249cc64cf63b8a22557cdc8537',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id'],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'table2',
|
||||
columns: [
|
||||
{
|
||||
name: 'parentId',
|
||||
tableName: 'table2',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
warnings: ['[@ForeignKeyConstraint.referenceColumns] Unable to find column (Table1.foo)'],
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Column, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools';
|
||||
|
||||
class Foo {}
|
||||
|
||||
@Table()
|
||||
@ForeignKeyConstraint({
|
||||
columns: ['parentId'],
|
||||
referenceTable: () => Foo,
|
||||
})
|
||||
export class Table1 {
|
||||
@Column()
|
||||
parentId!: string;
|
||||
}
|
||||
|
||||
export const description = 'should warn against missing reference table';
|
||||
export const schema: DatabaseSchema = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
name: 'parentId',
|
||||
tableName: 'table1',
|
||||
type: 'character varying',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
warnings: ['[@ForeignKeyConstraint.referenceTable] Unable to find table (Foo)'],
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
Column,
|
||||
DatabaseConstraintType,
|
||||
DatabaseSchema,
|
||||
ForeignKeyConstraint,
|
||||
PrimaryColumn,
|
||||
Table,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table()
|
||||
export class Table1 {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id1!: string;
|
||||
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id2!: string;
|
||||
}
|
||||
|
||||
@Table()
|
||||
@ForeignKeyConstraint({ columns: ['parentId1', 'parentId2'], referenceTable: () => Table1 })
|
||||
export class Table2 {
|
||||
@Column({ type: 'uuid' })
|
||||
parentId1!: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
parentId2!: string;
|
||||
}
|
||||
|
||||
export const description = 'should create a foreign key constraint to the target table';
|
||||
export const schema: DatabaseSchema = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
name: 'id1',
|
||||
tableName: 'table1',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: true,
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'id2',
|
||||
tableName: 'table1',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: true,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||
name: 'PK_e457e8b1301b7bc06ef78188ee4',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id1', 'id2'],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'table2',
|
||||
columns: [
|
||||
{
|
||||
name: 'parentId1',
|
||||
tableName: 'table2',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'parentId2',
|
||||
tableName: 'table2',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
name: 'IDX_aed36d04470eba20161aa8b1dc',
|
||||
tableName: 'table2',
|
||||
columnNames: ['parentId1', 'parentId2'],
|
||||
unique: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
name: 'FK_aed36d04470eba20161aa8b1dc6',
|
||||
tableName: 'table2',
|
||||
columnNames: ['parentId1', 'parentId2'],
|
||||
referenceColumnNames: ['id1', 'id2'],
|
||||
referenceTableName: 'table1',
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
Column,
|
||||
DatabaseConstraintType,
|
||||
DatabaseSchema,
|
||||
ForeignKeyConstraint,
|
||||
PrimaryColumn,
|
||||
Table,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table()
|
||||
export class Table1 {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
}
|
||||
|
||||
@Table()
|
||||
@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1, index: false })
|
||||
export class Table2 {
|
||||
@Column({ type: 'uuid' })
|
||||
parentId!: string;
|
||||
}
|
||||
|
||||
export const description = 'should create a foreign key constraint to the target table without an index';
|
||||
export const schema: DatabaseSchema = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
tableName: 'table1',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: true,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||
name: 'PK_b249cc64cf63b8a22557cdc8537',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id'],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'table2',
|
||||
columns: [
|
||||
{
|
||||
name: 'parentId',
|
||||
tableName: 'table2',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
name: 'FK_3fcca5cc563abf256fc346e3ff4',
|
||||
tableName: 'table2',
|
||||
columnNames: ['parentId'],
|
||||
referenceColumnNames: ['id'],
|
||||
referenceTableName: 'table1',
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Column, DatabaseConstraintType, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools';
|
||||
|
||||
@Table()
|
||||
export class Table1 {
|
||||
@Column()
|
||||
foo!: string;
|
||||
}
|
||||
|
||||
@Table()
|
||||
@ForeignKeyConstraint({
|
||||
columns: ['bar'],
|
||||
referenceTable: () => Table1,
|
||||
referenceColumns: ['foo'],
|
||||
})
|
||||
export class Table2 {
|
||||
@Column()
|
||||
bar!: string;
|
||||
}
|
||||
|
||||
export const description = 'should create a foreign key constraint to the target table without a primary key';
|
||||
export const schema: DatabaseSchema = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
name: 'foo',
|
||||
tableName: 'table1',
|
||||
type: 'character varying',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [],
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'table2',
|
||||
columns: [
|
||||
{
|
||||
name: 'bar',
|
||||
tableName: 'table2',
|
||||
type: 'character varying',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
name: 'IDX_7d9c784c98d12365d198d52e4e',
|
||||
tableName: 'table2',
|
||||
columnNames: ['bar'],
|
||||
unique: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
name: 'FK_7d9c784c98d12365d198d52e4e6',
|
||||
tableName: 'table2',
|
||||
columnNames: ['bar'],
|
||||
referenceTableName: 'table1',
|
||||
referenceColumnNames: ['foo'],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
96
server/test/sql-tools/foreign-key-constraint.stub.ts
Normal file
96
server/test/sql-tools/foreign-key-constraint.stub.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
Column,
|
||||
DatabaseConstraintType,
|
||||
DatabaseSchema,
|
||||
ForeignKeyConstraint,
|
||||
PrimaryColumn,
|
||||
Table,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table()
|
||||
export class Table1 {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
}
|
||||
|
||||
@Table()
|
||||
@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1 })
|
||||
export class Table2 {
|
||||
@Column({ type: 'uuid' })
|
||||
parentId!: string;
|
||||
}
|
||||
|
||||
export const description = 'should create a foreign key constraint to the target table';
|
||||
export const schema: DatabaseSchema = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [
|
||||
{
|
||||
name: 'table1',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
tableName: 'table1',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: true,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||
name: 'PK_b249cc64cf63b8a22557cdc8537',
|
||||
tableName: 'table1',
|
||||
columnNames: ['id'],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
{
|
||||
name: 'table2',
|
||||
columns: [
|
||||
{
|
||||
name: 'parentId',
|
||||
tableName: 'table2',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
isArray: false,
|
||||
primary: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
name: 'IDX_3fcca5cc563abf256fc346e3ff',
|
||||
tableName: 'table2',
|
||||
columnNames: ['parentId'],
|
||||
unique: false,
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
triggers: [],
|
||||
constraints: [
|
||||
{
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
name: 'FK_3fcca5cc563abf256fc346e3ff4',
|
||||
tableName: 'table2',
|
||||
columnNames: ['parentId'],
|
||||
referenceColumnNames: ['id'],
|
||||
referenceTableName: 'table1',
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
synchronize: true,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
@@ -22,7 +22,7 @@
|
||||
import { isEqual, omit } from 'lodash-es';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import maplibregl, { GlobeControl, type GeoJSONSource, type LngLatLike } from 'maplibre-gl';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onDestroy, onMount, untrack } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import {
|
||||
AttributionControl,
|
||||
@@ -61,7 +61,7 @@
|
||||
mapMarkers = $bindable(),
|
||||
showSettings = true,
|
||||
zoom = undefined,
|
||||
center = $bindable(undefined),
|
||||
center = undefined,
|
||||
hash = false,
|
||||
simplified = false,
|
||||
clickable = false,
|
||||
@@ -250,8 +250,14 @@
|
||||
});
|
||||
});
|
||||
|
||||
let lastCenter: LngLatLike | undefined = undefined;
|
||||
|
||||
$effect(() => {
|
||||
if (!map || !center || isEqual(lastCenter, center)) {
|
||||
return;
|
||||
}
|
||||
map?.jumpTo({ center, zoom });
|
||||
lastCenter = center;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
{#if sharedLinks.length > 0}
|
||||
<div class="flex justify-between items-center">
|
||||
<Text>{$t('shared_links')}</Text>
|
||||
<Link href={AppRoute.SHARED_LINKS} class="text-sm">{$t('view_all')}</Link>
|
||||
<Link href={AppRoute.SHARED_LINKS} onclick={() => onClose()} class="text-sm">{$t('view_all')}</Link>
|
||||
</div>
|
||||
|
||||
<Stack gap={4}>
|
||||
|
||||
Reference in New Issue
Block a user