Skip to content

Commit 85bdbc8

Browse files
authored
Merge pull request #25 from jacksonkasi1/dev
fix(engine): preserve runtime extensions via non-enumerable symbol during config cloning
2 parents 490bcbe + 7cf359d commit 85bdbc8

5 files changed

Lines changed: 94 additions & 29 deletions

File tree

bun.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/engine/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tablecraft/engine",
3-
"version": "0.1.6",
3+
"version": "0.1.7",
44
"description": "The backend query engine for TableCraft",
55
"type": "module",
66
"main": "dist/index.js",

packages/engine/src/define.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ import {
6767
introspectTable,
6868
} from "./utils/introspect";
6969

70+
export const TABLECRAFT_EXTENSIONS_KEY: unique symbol = Symbol.for("__tablecraft_ext") as any;
71+
72+
export type TableConfigWithExtensions<T extends Table = Table> = TableConfig & {
73+
[TABLECRAFT_EXTENSIONS_KEY]?: RuntimeExtensions<T>;
74+
};
75+
7076
type InferColumns<T> = T extends { _: { columns: infer C } }
7177
? keyof C & string
7278
: string;
@@ -113,7 +119,7 @@ export interface RuntimeExtensions<T extends Table = Table> {
113119
};
114120
}
115121

116-
function emptyExtensions<T extends Table = Table>(): RuntimeExtensions<T> {
122+
export function emptyExtensions<T extends Table = Table>(): RuntimeExtensions<T> {
117123
return {
118124
computedExpressions: new Map(),
119125
transforms: new Map(),
@@ -124,6 +130,25 @@ function emptyExtensions<T extends Table = Table>(): RuntimeExtensions<T> {
124130
rawOrderBys: [],
125131
ctes: new Map(),
126132
sqlJoinConditions: new Map(),
133+
countMode: undefined,
134+
};
135+
}
136+
137+
function cloneExtensions<T extends Table = Table>(
138+
ext: RuntimeExtensions<T> = emptyExtensions<T>(),
139+
): RuntimeExtensions<T> {
140+
return {
141+
...ext,
142+
computedExpressions: new Map(ext.computedExpressions),
143+
transforms: new Map(ext.transforms),
144+
rawSelects: new Map(ext.rawSelects),
145+
rawWheres: [...ext.rawWheres],
146+
dynamicWheres: [...ext.dynamicWheres],
147+
rawJoins: [...ext.rawJoins],
148+
rawOrderBys: [...ext.rawOrderBys],
149+
ctes: new Map(ext.ctes),
150+
sqlJoinConditions: new Map(ext.sqlJoinConditions),
151+
hooks: ext.hooks ? { ...ext.hooks } : undefined,
127152
};
128153
}
129154

@@ -134,10 +159,12 @@ export class TableDefinitionBuilder<T extends Table = Table> {
134159
_table: T;
135160
_ext: RuntimeExtensions<T>;
136161

137-
constructor(table: T, config: TableConfig) {
162+
constructor(table: T, config: TableConfigWithExtensions<T>) {
138163
this._table = table;
139164
this._config = config;
140-
this._ext = emptyExtensions();
165+
this._ext = cloneExtensions(
166+
config[TABLECRAFT_EXTENSIONS_KEY] ?? emptyExtensions<T>(),
167+
);
141168
}
142169

143170
// ──── Column Format / Metadata ────
@@ -1049,19 +1076,29 @@ export class TableDefinitionBuilder<T extends Table = Table> {
10491076

10501077
// ──── Output ────
10511078

1052-
toConfig(): TableConfig {
1053-
return { ...this._config };
1079+
toConfig(): TableConfigWithExtensions<T> {
1080+
const config = { ...this._config } as TableConfigWithExtensions<T>;
1081+
Object.defineProperty(config, TABLECRAFT_EXTENSIONS_KEY, {
1082+
value: cloneExtensions(this._ext),
1083+
enumerable: true,
1084+
configurable: true,
1085+
writable: true,
1086+
});
1087+
return config;
10541088
}
10551089
}
10561090

10571091
// ── Main Entry ──
10581092

10591093
export function defineTable<T extends Table>(
10601094
table: T,
1061-
options?: QuickOptions<T> | TableConfig,
1095+
options?: QuickOptions<T> | TableConfigWithExtensions<T>,
10621096
): TableDefinitionBuilder<T> {
10631097
if (options && "columns" in options && Array.isArray(options.columns)) {
1064-
return new TableDefinitionBuilder(table, options as TableConfig);
1098+
return new TableDefinitionBuilder(
1099+
table,
1100+
options as TableConfigWithExtensions<T>,
1101+
);
10651102
}
10661103

10671104
const config = introspectTable(table);

packages/engine/src/engine.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ import { FieldSelector } from './core/fieldSelector';
3131
import { detectDialect, supportsFeature, Dialect } from './core/dialect';
3232
import { formatResponse, applyJsTransforms } from './utils/responseFormatter';
3333
import { exportData } from './utils/export';
34-
import { TableDefinitionBuilder, RuntimeExtensions, drizzleOperators } from './define';
34+
import {
35+
TableDefinitionBuilder,
36+
RuntimeExtensions,
37+
TABLECRAFT_EXTENSIONS_KEY,
38+
drizzleOperators,
39+
emptyExtensions,
40+
} from './define';
3541
import { TableCraftError, QueryError, DialectError } from './errors';
3642
import { applyRoleBasedVisibility } from './core/roleFilter';
3743
import { buildMetadata } from './core/metadataBuilder';
@@ -44,24 +50,25 @@ function resolveInput(input: ConfigInput): {
4450
config: TableConfig;
4551
ext: RuntimeExtensions<any>;
4652
} {
47-
if (input && typeof input === 'object' && '_config' in input && '_ext' in input) {
53+
if (!input) {
54+
throw new TableCraftError('Invalid input: Table configuration is required', 'VALIDATION_ERROR');
55+
}
56+
57+
if (typeof input === 'object' && '_config' in input && '_ext' in input) {
4858
const b = input as TableDefinitionBuilder<any>;
49-
return { config: b.toConfig(), ext: b._ext as RuntimeExtensions<any> };
59+
const cfg = b.toConfig();
60+
return { config: cfg, ext: cfg[TABLECRAFT_EXTENSIONS_KEY] as RuntimeExtensions<any> };
5061
}
62+
63+
const plainConfig = input as TableConfig & {
64+
[TABLECRAFT_EXTENSIONS_KEY]?: RuntimeExtensions<any>;
65+
};
66+
67+
const embeddedExt = plainConfig?.[TABLECRAFT_EXTENSIONS_KEY];
68+
5169
return {
52-
config: input as TableConfig,
53-
ext: {
54-
computedExpressions: new Map(),
55-
transforms: new Map(),
56-
rawSelects: new Map(),
57-
rawWheres: [],
58-
dynamicWheres: [],
59-
rawJoins: [],
60-
rawOrderBys: [],
61-
ctes: new Map(),
62-
sqlJoinConditions: new Map(),
63-
countMode: undefined,
64-
},
70+
config: plainConfig,
71+
ext: embeddedExt ?? emptyExtensions(),
6572
};
6673
}
6774

packages/engine/test/columnMeta.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { defineTable } from '../src/define';
2+
import { defineTable, TABLECRAFT_EXTENSIONS_KEY } from '../src/define';
33
import { sql } from 'drizzle-orm';
44
import { pgTable, uuid, varchar, integer, timestamp } from 'drizzle-orm/pg-core';
55

@@ -102,4 +102,25 @@ describe('columnMeta', () => {
102102
expect((col as any).width).toBe(150); // added by .columnMeta()
103103
expect((col as any).minWidth).toBe(100);
104104
});
105-
});
105+
106+
it('should rehydrate runtime extensions from toConfig output', () => {
107+
const beforeQuery = (params: any) => params;
108+
109+
const builder = defineTable(orders)
110+
.computed('profit', sql`${orders.total} - ${orders.cost}`)
111+
.transform('status', (value) => String(value).toUpperCase())
112+
.beforeQuery(beforeQuery);
113+
114+
const config = builder.toConfig();
115+
const runtimeExt = config[TABLECRAFT_EXTENSIONS_KEY]!;
116+
117+
const rebuilt = defineTable(orders, { ...config });
118+
119+
expect(rebuilt._ext.computedExpressions.has('profit')).toBe(true);
120+
expect(rebuilt._ext.transforms.has('status')).toBe(true);
121+
expect(rebuilt._ext.hooks?.beforeQuery).toBe(beforeQuery);
122+
123+
rebuilt.rawWhere(sql`1 = 1`);
124+
expect(runtimeExt.rawWheres).toHaveLength(0);
125+
});
126+
});

0 commit comments

Comments
 (0)