Skip to content

Commit 792f8d4

Browse files
committed
chore: add dbFieldName dbTableName to domain
1 parent bc22f58 commit 792f8d4

File tree

24 files changed

+481
-254
lines changed

24 files changed

+481
-254
lines changed

.dockerignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ tmp
4545

4646
# other
4747
**/db
48+
!packages/v2/adapter-postgres-state/src/db
49+
!packages/v2/adapter-postgres-state/src/db/**
4850
**/.assets
4951
**/.temporary
5052
**.DS_Store
5153
docs
52-
**/*.md
54+
**/*.md

agents.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Practical exceptions that are required by the architecture:
9090
- `neverthrow` error side uses strings (e.g. `Result<T, string>`).
9191
- The Specification interface requires `isSatisfiedBy(...): boolean`.
9292
- Value Objects may expose `toString()` / `toDate()` / `toNumber()` for adapter/serialization boundaries (avoid using these in domain logic).
93+
- Rehydration-only Value Objects (e.g. `DbTableName`, `DbFieldName`) must extend `RehydratedValueObject`; create empty placeholders in domain, set real values only via repository rehydrate, and return `err(...)` when accessed before rehydrate.
9394

9495
## Builders/factories (non-negotiable)
9596

apps/nestjs-backend/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"moduleResolution": "Node16",
99
"declaration": true,
1010
"declarationDir": "./dist",
11-
"module": "CommonJS",
11+
"module": "Node16",
1212
"noEmit": false,
1313
"sourceMap": true,
1414
"allowJs": false,

packages/v2/adapter-postgres-ddl/src/naming.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ export const joinDbTableName = (schemaName: string, tableName: string): string =
2929
return `${schemaName}.${tableName}`;
3030
};
3131

32-
export const splitDbTableName = (
33-
dbTableName: string
34-
): { schema: string | null; tableName: string } => {
35-
const dotIndex = dbTableName.indexOf('.');
36-
if (dotIndex === -1) return { schema: null, tableName: dbTableName };
37-
return { schema: dbTableName.slice(0, dotIndex), tableName: dbTableName.slice(dotIndex + 1) };
38-
};
39-
4032
export const ensureUniqueDbFieldName = (baseName: string, reservedNames: Set<string>): string => {
4133
if (!reservedNames.has(baseName)) return baseName;
4234

@@ -49,19 +41,3 @@ export const ensureUniqueDbFieldName = (baseName: string, reservedNames: Set<str
4941

5042
return candidate;
5143
};
52-
53-
export const buildDbFieldNameMap = (
54-
fields: ReadonlyArray<{ id: string; name: string }>
55-
): Map<string, string> => {
56-
const reservedNames = new Set(baseRecordColumnNames);
57-
const fieldDbNameById = new Map<string, string>();
58-
59-
for (const field of fields) {
60-
const baseName = convertNameToValidCharacter(field.name, 40);
61-
const dbFieldName = ensureUniqueDbFieldName(baseName, reservedNames);
62-
reservedNames.add(dbFieldName);
63-
fieldDbNameById.set(field.id, dbFieldName);
64-
}
65-
66-
return fieldDbNameById;
67-
};

packages/v2/adapter-postgres-ddl/src/repositories/PostgresTableSchemaRepository.spec.ts

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
/* eslint-disable sonarjs/cognitive-complexity */
12
/* eslint-disable @typescript-eslint/naming-convention */
23
import type { IExecutionContext, ITableSchemaRepository } from '@teable/v2-core';
34
import {
45
ActorId,
56
BaseId,
7+
DbFieldName,
8+
DbTableName,
69
FieldName,
710
RatingMax,
811
SelectOption,
@@ -22,21 +25,11 @@ type StartedPostgreSqlContainer = Awaited<ReturnType<PostgreSqlContainer['start'
2225
import { registerV2PostgresDdlAdapter } from '../di/register';
2326
import {
2427
baseRecordColumnNames,
25-
buildDbFieldNameMap,
2628
convertNameToValidCharacter,
29+
ensureUniqueDbFieldName,
2730
joinDbTableName,
28-
splitDbTableName,
2931
} from '../naming';
3032

31-
interface ITestTableMetaTable {
32-
id: string;
33-
db_table_name: string;
34-
}
35-
36-
interface ITestDatabase {
37-
table_meta: ITestTableMetaTable;
38-
}
39-
4033
describe('PostgresTableSchemaRepository (pg)', () => {
4134
let pgContainer: StartedPostgreSqlContainer;
4235

@@ -58,7 +51,7 @@ describe('PostgresTableSchemaRepository (pg)', () => {
5851
pg: { connectionString: pgContainer.getConnectionUri() },
5952
});
6053

61-
const db = c.resolve<Kysely<ITestDatabase>>(v2PostgresDbTokens.db);
54+
const db = c.resolve<Kysely<unknown>>(v2PostgresDbTokens.db);
6255
const repo = c.resolve<ITableSchemaRepository>(v2CoreTokens.tableSchemaRepository);
6356

6457
try {
@@ -109,23 +102,28 @@ describe('PostgresTableSchemaRepository (pg)', () => {
109102
baseId.toString(),
110103
convertNameToValidCharacter(table.name().toString(), 40)
111104
);
112-
const fieldDbNameById = buildDbFieldNameMap(
113-
table
114-
.fields()
115-
.map((field) => ({ id: field.id().toString(), name: field.name().toString() }))
116-
);
117-
118-
await db.schema
119-
.createTable('table_meta')
120-
.ifNotExists()
121-
.addColumn('id', 'text', (col) => col.primaryKey())
122-
.addColumn('db_table_name', 'text', (col) => col.notNull())
123-
.execute();
124-
125-
await db
126-
.insertInto('table_meta')
127-
.values({ id: table.id().toString(), db_table_name: dbTableName })
128-
.execute();
105+
const reservedNames = new Set(baseRecordColumnNames);
106+
const fieldDbNames: string[] = [];
107+
108+
const dbTableNameResult = DbTableName.rehydrate(dbTableName);
109+
expect(dbTableNameResult.isOk()).toBe(true);
110+
if (dbTableNameResult.isErr()) return;
111+
const setTableDbNameResult = table.setDbTableName(dbTableNameResult.value);
112+
expect(setTableDbNameResult.isOk()).toBe(true);
113+
if (setTableDbNameResult.isErr()) return;
114+
115+
for (const field of table.fields()) {
116+
const baseName = convertNameToValidCharacter(field.name().toString(), 40);
117+
const dbFieldName = ensureUniqueDbFieldName(baseName, reservedNames);
118+
reservedNames.add(dbFieldName);
119+
fieldDbNames.push(dbFieldName);
120+
const dbFieldNameResult = DbFieldName.rehydrate(dbFieldName);
121+
expect(dbFieldNameResult.isOk()).toBe(true);
122+
if (dbFieldNameResult.isErr()) return;
123+
const setFieldDbNameResult = field.setDbFieldName(dbFieldNameResult.value);
124+
expect(setFieldDbNameResult.isOk()).toBe(true);
125+
if (setFieldDbNameResult.isErr()) return;
126+
}
129127

130128
const actorIdResult = ActorId.create('system');
131129
expect(actorIdResult.isOk()).toBe(true);
@@ -137,9 +135,12 @@ describe('PostgresTableSchemaRepository (pg)', () => {
137135
if (insertResult.isErr()) return;
138136

139137
const expectedBaseColumns = baseRecordColumnNames;
140-
const expectedFieldColumns = [...fieldDbNameById.values()];
138+
const expectedFieldColumns = fieldDbNames;
141139

142-
const { schema, tableName } = splitDbTableName(dbTableName);
140+
const splitResult = dbTableNameResult.value.split({ defaultSchema: 'public' });
141+
expect(splitResult.isOk()).toBe(true);
142+
if (splitResult.isErr()) return;
143+
const { schema, tableName } = splitResult.value;
143144
const schemaName = schema ?? 'public';
144145
const columnsResult = await sql<{ columnName: string }>`
145146
select column_name as "columnName"

packages/v2/adapter-postgres-ddl/src/repositories/PostgresTableSchemaRepository.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { sql } from 'kysely';
1111
import { err, ok } from 'neverthrow';
1212
import type { Result } from 'neverthrow';
1313

14-
import { buildDbFieldNameMap, splitDbTableName } from '../naming';
1514
import {
1615
PostgresTableFieldVisitor,
1716
type ICreateTableBuilderRef,
@@ -26,27 +25,12 @@ export class PostgresTableSchemaRepository implements ITableSchemaRepository {
2625

2726
@TraceSpan()
2827
async insert(context: IExecutionContext, table: Table): Promise<Result<void, string>> {
29-
const tableId = table.id().toString();
30-
let dbTableName: string;
31-
const fieldDbNameById = buildDbFieldNameMap(
32-
table.fields().map((field) => ({ id: field.id().toString(), name: field.name().toString() }))
33-
);
28+
const dbTableNameResult = table
29+
.dbTableName()
30+
.andThen((name) => name.split({ defaultSchema: null }));
31+
if (dbTableNameResult.isErr()) return err(dbTableNameResult.error);
32+
const { schema, tableName } = dbTableNameResult.value;
3433
const db = resolvePostgresDb(this.db, context);
35-
try {
36-
const tableMetaResult = await sql<{ dbTableName: string }>`
37-
select db_table_name as "dbTableName"
38-
from table_meta
39-
where id = ${tableId}
40-
`.execute(db);
41-
42-
dbTableName = tableMetaResult.rows[0]?.dbTableName ?? '';
43-
} catch (error) {
44-
return err(`Failed to load table metadata: ${describeError(error)}`);
45-
}
46-
47-
if (!dbTableName) return err('Missing db table name');
48-
49-
const { schema, tableName } = splitDbTableName(dbTableName);
5034

5135
type ICreateTableBuilder = CreateTableBuilder<string, string>;
5236
const schemaBuilder = schema ? db.schema.withSchema(schema) : db.schema;
@@ -64,7 +48,7 @@ export class PostgresTableSchemaRepository implements ITableSchemaRepository {
6448
.addColumn('__version', 'integer', (col: ColumnDefinitionBuilder) => col.notNull());
6549

6650
const builderRef: ICreateTableBuilderRef = { builder };
67-
const visitor = new PostgresTableFieldVisitor(builderRef, fieldDbNameById);
51+
const visitor = new PostgresTableFieldVisitor(builderRef);
6852
const applyResult = visitor.apply(table);
6953
if (applyResult.isErr()) return err(applyResult.error);
7054

packages/v2/adapter-postgres-ddl/src/visitors/PostgresTableFieldVisitor.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ export interface ICreateTableBuilderRef {
2626
}
2727

2828
export class PostgresTableFieldVisitor implements IFieldVisitor<void> {
29-
constructor(
30-
private readonly builderRef: ICreateTableBuilderRef,
31-
private readonly fieldDbNameById: Map<string, string>
32-
) {}
29+
constructor(private readonly builderRef: ICreateTableBuilderRef) {}
3330

3431
private static isFieldArray(value: Table | ReadonlyArray<Field>): value is ReadonlyArray<Field> {
3532
return Array.isArray(value);
@@ -51,52 +48,57 @@ export class PostgresTableFieldVisitor implements IFieldVisitor<void> {
5148
}
5249

5350
visitSingleLineTextField(field: SingleLineTextField): Result<void, string> {
54-
return this.addColumn(field.id().toString(), 'text');
51+
return this.addColumn(field, 'text');
5552
}
5653

5754
visitLongTextField(field: LongTextField): Result<void, string> {
58-
return this.addColumn(field.id().toString(), 'text');
55+
return this.addColumn(field, 'text');
5956
}
6057

6158
visitNumberField(field: NumberField): Result<void, string> {
62-
return this.addColumn(field.id().toString(), 'numeric');
59+
return this.addColumn(field, 'numeric');
6360
}
6461

6562
visitRatingField(field: RatingField): Result<void, string> {
66-
return this.addColumn(field.id().toString(), 'numeric');
63+
return this.addColumn(field, 'numeric');
6764
}
6865

6966
visitSingleSelectField(field: SingleSelectField): Result<void, string> {
70-
return this.addColumn(field.id().toString(), 'text');
67+
return this.addColumn(field, 'text');
7168
}
7269

7370
visitMultipleSelectField(field: MultipleSelectField): Result<void, string> {
74-
return this.addColumn(field.id().toString(), 'jsonb');
71+
return this.addColumn(field, 'jsonb');
7572
}
7673

7774
visitCheckboxField(field: CheckboxField): Result<void, string> {
78-
return this.addColumn(field.id().toString(), 'boolean');
75+
return this.addColumn(field, 'boolean');
7976
}
8077

8178
visitAttachmentField(field: AttachmentField): Result<void, string> {
82-
return this.addColumn(field.id().toString(), 'jsonb');
79+
return this.addColumn(field, 'jsonb');
8380
}
8481

8582
visitDateField(field: DateField): Result<void, string> {
86-
return this.addColumn(field.id().toString(), 'timestamptz');
83+
return this.addColumn(field, 'timestamptz');
8784
}
8885

8986
visitUserField(field: UserField): Result<void, string> {
90-
return this.addColumn(field.id().toString(), 'jsonb');
87+
return this.addColumn(field, 'jsonb');
9188
}
9289

9390
visitButtonField(field: ButtonField): Result<void, string> {
94-
return this.addColumn(field.id().toString(), 'jsonb');
91+
return this.addColumn(field, 'jsonb');
9592
}
9693

97-
private addColumn(fieldId: string, dataType: ITableColumnDataType): Result<void, string> {
98-
const columnName = this.fieldDbNameById.get(fieldId);
99-
if (!columnName) return err(`Missing db field name for field ${fieldId}`);
94+
private addColumn(field: Field, dataType: ITableColumnDataType): Result<void, string> {
95+
const columnNameResult = field.dbFieldName().andThen((name) => name.value());
96+
if (columnNameResult.isErr()) {
97+
return err(
98+
`Missing db field name for field ${field.id().toString()}: ${columnNameResult.error}`
99+
);
100+
}
101+
const columnName = columnNameResult.value;
100102
this.builderRef.builder = this.builderRef.builder.addColumn(
101103
columnName,
102104
dataType

packages/v2/adapter-postgres-state/src/naming.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,3 @@ export const convertNameToValidCharacter = (name: string, maxLength = 40): strin
2828
export const joinDbTableName = (schemaName: string, tableName: string): string => {
2929
return `${schemaName}.${tableName}`;
3030
};
31-
32-
export const splitDbTableName = (dbTableName: string): { schema: string; table: string } => {
33-
const dotIndex = dbTableName.indexOf('.');
34-
if (dotIndex === -1) return { schema: 'public', table: dbTableName };
35-
return {
36-
schema: dbTableName.slice(0, dotIndex),
37-
table: dbTableName.slice(dotIndex + 1),
38-
};
39-
};

packages/v2/adapter-postgres-state/src/repositories/PostgresTableRepository.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ describe('PostgresTableRepository (pg)', () => {
247247
const insertResult = await repo.insert(context, table);
248248
expect(insertResult.isOk()).toBe(true);
249249
if (insertResult.isErr()) return;
250+
const persistedTable = insertResult.value;
250251

251252
const persistedFields = await db
252253
.selectFrom('field')
@@ -263,6 +264,11 @@ describe('PostgresTableRepository (pg)', () => {
263264
table.name().toString(),
264265
40
265266
)}`;
267+
const dbTableNameResult = persistedTable.dbTableName().andThen((name) => name.value());
268+
expect(dbTableNameResult.isOk()).toBe(true);
269+
if (dbTableNameResult.isErr()) return;
270+
expect(dbTableNameResult.value).toBe(expectedDbTableName);
271+
266272
const tableMetaRow = await db
267273
.selectFrom('table_meta')
268274
.select(['db_table_name', 'base_id'])
@@ -274,6 +280,15 @@ describe('PostgresTableRepository (pg)', () => {
274280
const expectedDbFieldNames = table
275281
.fields()
276282
.map((field) => convertNameToValidCharacter(field.name().toString(), 40));
283+
const dbFieldNameResults = persistedTable
284+
.fields()
285+
.map((field) => field.dbFieldName().andThen((name) => name.value()));
286+
expect(dbFieldNameResults.every((result) => result.isOk())).toBe(true);
287+
if (dbFieldNameResults.some((result) => result.isErr())) return;
288+
expect(dbFieldNameResults.map((result) => result._unsafeUnwrap())).toEqual(
289+
expectedDbFieldNames
290+
);
291+
277292
const dbFieldRows = await db
278293
.selectFrom('field')
279294
.select(['db_field_name'])
@@ -294,6 +309,10 @@ describe('PostgresTableRepository (pg)', () => {
294309
expect(loaded.id().toString()).toBe(table.id().toString());
295310
expect(loaded.name().toString()).toBe(table.name().toString());
296311
expect(loaded.primaryFieldId().equals(table.primaryFieldId())).toBe(true);
312+
const loadedDbTableNameResult = loaded.dbTableName().andThen((name) => name.value());
313+
expect(loadedDbTableNameResult.isOk()).toBe(true);
314+
if (loadedDbTableNameResult.isErr()) return;
315+
expect(loadedDbTableNameResult.value).toBe(expectedDbTableName);
297316
expect(
298317
loaded.views().map((v) => ({ name: v.name().toString(), type: v.type().toString() }))
299318
).toEqual([

0 commit comments

Comments
 (0)