mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat(server): add property-type to DuckDB column-spec mapping
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { BasePropertyType } from '../base.schemas';
|
||||
import { buildColumnSpecs, SYSTEM_COLUMNS } from './column-types';
|
||||
|
||||
const p = (type: string, extra: Record<string, unknown> = {}) => ({
|
||||
id: `prop-${type}`,
|
||||
type,
|
||||
typeOptions: extra,
|
||||
}) as any;
|
||||
|
||||
describe('buildColumnSpecs', () => {
|
||||
it('includes the fixed system columns first', () => {
|
||||
const specs = buildColumnSpecs([]);
|
||||
expect(specs.map((s) => s.column)).toEqual(SYSTEM_COLUMNS.map((s) => s.column));
|
||||
});
|
||||
|
||||
it('maps text / url / email to VARCHAR indexable', () => {
|
||||
for (const t of [BasePropertyType.TEXT, BasePropertyType.URL, BasePropertyType.EMAIL]) {
|
||||
const specs = buildColumnSpecs([p(t)]);
|
||||
const user = specs[specs.length - 1];
|
||||
expect(user.ddlType).toBe('VARCHAR');
|
||||
expect(user.indexable).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('maps number to DOUBLE indexable', () => {
|
||||
const specs = buildColumnSpecs([p(BasePropertyType.NUMBER)]);
|
||||
const user = specs[specs.length - 1];
|
||||
expect(user.ddlType).toBe('DOUBLE');
|
||||
expect(user.indexable).toBe(true);
|
||||
});
|
||||
|
||||
it('maps date to TIMESTAMPTZ indexable', () => {
|
||||
const specs = buildColumnSpecs([p(BasePropertyType.DATE)]);
|
||||
const user = specs[specs.length - 1];
|
||||
expect(user.ddlType).toBe('TIMESTAMPTZ');
|
||||
expect(user.indexable).toBe(true);
|
||||
});
|
||||
|
||||
it('maps checkbox to BOOLEAN indexable', () => {
|
||||
const specs = buildColumnSpecs([p(BasePropertyType.CHECKBOX)]);
|
||||
const user = specs[specs.length - 1];
|
||||
expect(user.ddlType).toBe('BOOLEAN');
|
||||
});
|
||||
|
||||
it('maps select / status to VARCHAR indexable', () => {
|
||||
for (const t of [BasePropertyType.SELECT, BasePropertyType.STATUS]) {
|
||||
const specs = buildColumnSpecs([p(t)]);
|
||||
const user = specs[specs.length - 1];
|
||||
expect(user.ddlType).toBe('VARCHAR');
|
||||
expect(user.indexable).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('maps multiSelect / file / multi-person to JSON non-indexable', () => {
|
||||
for (const t of [BasePropertyType.MULTI_SELECT, BasePropertyType.FILE]) {
|
||||
const specs = buildColumnSpecs([p(t)]);
|
||||
const user = specs[specs.length - 1];
|
||||
expect(user.ddlType).toBe('JSON');
|
||||
expect(user.indexable).toBe(false);
|
||||
}
|
||||
const specs = buildColumnSpecs([p(BasePropertyType.PERSON, { allowMultiple: true })]);
|
||||
expect(specs[specs.length - 1].ddlType).toBe('JSON');
|
||||
});
|
||||
|
||||
it('maps single-person to VARCHAR indexable when allowMultiple=false', () => {
|
||||
const specs = buildColumnSpecs([p(BasePropertyType.PERSON, { allowMultiple: false })]);
|
||||
const user = specs[specs.length - 1];
|
||||
expect(user.ddlType).toBe('VARCHAR');
|
||||
expect(user.indexable).toBe(true);
|
||||
});
|
||||
|
||||
it('skips unknown property types', () => {
|
||||
const specs = buildColumnSpecs([p('unknown-type-x')]);
|
||||
expect(specs.length).toBe(SYSTEM_COLUMNS.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BasePropertyType, BasePropertyTypeValue } from '../base.schemas';
|
||||
import { ColumnSpec } from './query-cache.types';
|
||||
import type { BaseProperty } from '@docmost/db/types/entity.types';
|
||||
|
||||
export const SYSTEM_COLUMNS: ColumnSpec[] = [
|
||||
{ column: 'id', ddlType: 'VARCHAR', indexable: false },
|
||||
{ column: 'position', ddlType: 'VARCHAR', indexable: true },
|
||||
{ column: 'created_at', ddlType: 'TIMESTAMPTZ', indexable: true },
|
||||
{ column: 'updated_at', ddlType: 'TIMESTAMPTZ', indexable: true },
|
||||
{ column: 'last_updated_by_id', ddlType: 'VARCHAR', indexable: true },
|
||||
{ column: 'deleted_at', ddlType: 'TIMESTAMPTZ', indexable: false },
|
||||
{ column: 'search_text', ddlType: 'VARCHAR', indexable: false },
|
||||
];
|
||||
|
||||
type PropertyLike = Pick<BaseProperty, 'id' | 'type' | 'typeOptions'>;
|
||||
|
||||
export function buildColumnSpecs(properties: PropertyLike[]): ColumnSpec[] {
|
||||
const out: ColumnSpec[] = [...SYSTEM_COLUMNS];
|
||||
for (const prop of properties) {
|
||||
const spec = buildUserColumn(prop);
|
||||
if (spec) out.push(spec);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildUserColumn(prop: PropertyLike): ColumnSpec | null {
|
||||
const t = prop.type as BasePropertyTypeValue;
|
||||
switch (t) {
|
||||
case BasePropertyType.TEXT:
|
||||
case BasePropertyType.URL:
|
||||
case BasePropertyType.EMAIL:
|
||||
return { column: prop.id, ddlType: 'VARCHAR', indexable: true, property: prop };
|
||||
case BasePropertyType.NUMBER:
|
||||
return { column: prop.id, ddlType: 'DOUBLE', indexable: true, property: prop };
|
||||
case BasePropertyType.DATE:
|
||||
return { column: prop.id, ddlType: 'TIMESTAMPTZ', indexable: true, property: prop };
|
||||
case BasePropertyType.CHECKBOX:
|
||||
return { column: prop.id, ddlType: 'BOOLEAN', indexable: true, property: prop };
|
||||
case BasePropertyType.SELECT:
|
||||
case BasePropertyType.STATUS:
|
||||
return { column: prop.id, ddlType: 'VARCHAR', indexable: true, property: prop };
|
||||
case BasePropertyType.MULTI_SELECT:
|
||||
case BasePropertyType.FILE:
|
||||
return { column: prop.id, ddlType: 'JSON', indexable: false, property: prop };
|
||||
case BasePropertyType.PERSON: {
|
||||
const allowMultiple = !!(prop.typeOptions as any)?.allowMultiple;
|
||||
return allowMultiple
|
||||
? { column: prop.id, ddlType: 'JSON', indexable: false, property: prop }
|
||||
: { column: prop.id, ddlType: 'VARCHAR', indexable: true, property: prop };
|
||||
}
|
||||
// System types are modelled as system columns on base_rows — do not add
|
||||
// a per-property column for them. They're already in SYSTEM_COLUMNS.
|
||||
case BasePropertyType.CREATED_AT:
|
||||
case BasePropertyType.LAST_EDITED_AT:
|
||||
case BasePropertyType.LAST_EDITED_BY:
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { DuckDBConnection, DuckDBInstance } from '@duckdb/node-api';
|
||||
import type { BaseProperty } from '@docmost/db/types/entity.types';
|
||||
|
||||
export type DuckDbColumnType =
|
||||
| 'VARCHAR'
|
||||
| 'DOUBLE'
|
||||
| 'BOOLEAN'
|
||||
| 'TIMESTAMPTZ'
|
||||
| 'JSON';
|
||||
|
||||
export type ColumnSpec = {
|
||||
// The uuid of the property (user-defined props) or a stable literal
|
||||
// ('id', 'position', 'created_at', 'updated_at', 'last_updated_by_id',
|
||||
// 'deleted_at', 'search_text') for system columns.
|
||||
column: string;
|
||||
ddlType: DuckDbColumnType;
|
||||
indexable: boolean;
|
||||
// For user-defined props we keep the source BaseProperty so callers can
|
||||
// resolve the extraction rule from JSON.
|
||||
property?: Pick<BaseProperty, 'id' | 'type' | 'typeOptions'>;
|
||||
};
|
||||
|
||||
export type LoadedCollection = {
|
||||
baseId: string;
|
||||
schemaVersion: number;
|
||||
columns: ColumnSpec[];
|
||||
instance: DuckDBInstance;
|
||||
connection: DuckDBConnection;
|
||||
lastAccessedAt: number;
|
||||
};
|
||||
|
||||
export type ChangeEnvelope =
|
||||
| { kind: 'row-upsert'; baseId: string; row: Record<string, unknown> }
|
||||
| { kind: 'row-delete'; baseId: string; rowId: string }
|
||||
| { kind: 'rows-delete'; baseId: string; rowIds: string[] }
|
||||
| { kind: 'row-reorder'; baseId: string; rowId: string; position: string }
|
||||
| { kind: 'schema-invalidate'; baseId: string; schemaVersion: number };
|
||||
Reference in New Issue
Block a user