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