@hatk/hatk 0.0.1-alpha.22 → 0.0.1-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/backfill.d.ts.map +1 -1
  2. package/dist/backfill.js +16 -4
  3. package/dist/cli.js +21 -33
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +1 -0
  7. package/dist/database/adapter-factory.d.ts +6 -0
  8. package/dist/database/adapter-factory.d.ts.map +1 -0
  9. package/dist/database/adapter-factory.js +20 -0
  10. package/dist/database/adapters/duckdb-search.d.ts +12 -0
  11. package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
  12. package/dist/database/adapters/duckdb-search.js +27 -0
  13. package/dist/database/adapters/duckdb.d.ts +25 -0
  14. package/dist/database/adapters/duckdb.d.ts.map +1 -0
  15. package/dist/database/adapters/duckdb.js +161 -0
  16. package/dist/database/adapters/sqlite-search.d.ts +18 -0
  17. package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
  18. package/dist/database/adapters/sqlite-search.js +38 -0
  19. package/dist/database/adapters/sqlite.d.ts +18 -0
  20. package/dist/database/adapters/sqlite.d.ts.map +1 -0
  21. package/dist/database/adapters/sqlite.js +87 -0
  22. package/dist/database/db.d.ts +149 -0
  23. package/dist/database/db.d.ts.map +1 -0
  24. package/dist/database/db.js +1456 -0
  25. package/dist/database/dialect.d.ts +45 -0
  26. package/dist/database/dialect.d.ts.map +1 -0
  27. package/dist/database/dialect.js +72 -0
  28. package/dist/database/fts.d.ts +24 -0
  29. package/dist/database/fts.d.ts.map +1 -0
  30. package/dist/database/fts.js +777 -0
  31. package/dist/database/index.d.ts +7 -0
  32. package/dist/database/index.d.ts.map +1 -0
  33. package/dist/database/index.js +6 -0
  34. package/dist/database/ports.d.ts +44 -0
  35. package/dist/database/ports.d.ts.map +1 -0
  36. package/dist/database/ports.js +1 -0
  37. package/dist/database/schema.d.ts +60 -0
  38. package/dist/database/schema.d.ts.map +1 -0
  39. package/dist/database/schema.js +388 -0
  40. package/dist/feeds.js +1 -1
  41. package/dist/hooks.js +1 -1
  42. package/dist/hydrate.js +1 -1
  43. package/dist/indexer.d.ts.map +1 -1
  44. package/dist/indexer.js +3 -3
  45. package/dist/labels.js +2 -2
  46. package/dist/main.js +30 -10
  47. package/dist/oauth/db.d.ts.map +1 -1
  48. package/dist/oauth/db.js +41 -15
  49. package/dist/oauth/server.js +4 -4
  50. package/dist/opengraph.js +1 -1
  51. package/dist/seed.js +1 -1
  52. package/dist/server.js +4 -4
  53. package/dist/setup.d.ts +10 -1
  54. package/dist/setup.d.ts.map +1 -1
  55. package/dist/setup.js +2 -2
  56. package/dist/test.d.ts +1 -1
  57. package/dist/test.d.ts.map +1 -1
  58. package/dist/test.js +22 -8
  59. package/dist/views.js +1 -1
  60. package/dist/vite-plugin.d.ts.map +1 -1
  61. package/dist/vite-plugin.js +10 -0
  62. package/dist/xrpc.js +2 -2
  63. package/package.json +3 -1
@@ -0,0 +1,7 @@
1
+ export type { DatabasePort, BulkInserter, SearchPort, Dialect } from './ports.ts';
2
+ export type { SqlDialect } from './dialect.ts';
3
+ export { getDialect, DUCKDB_DIALECT, SQLITE_DIALECT } from './dialect.ts';
4
+ export { createAdapter } from './adapter-factory.ts';
5
+ export { initDatabase, closeDatabase, querySQL, runSQL, insertRecord, deleteRecord, queryRecords, searchRecords, getRecordByUri, getCursor, setCursor, bulkInsertRecords, packCursor, unpackCursor, } from './db.ts';
6
+ export { type TableSchema, type ColumnDef, type ChildTableSchema, loadLexicons, discoverCollections, buildSchemas, generateTableSchema, generateCreateTableSQL, toSnakeCase, getLexicon, getLexiconArray, getAllLexicons, storeLexicons, } from './schema.ts';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACjF,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA;AAGhB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,UAAU,EACV,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,aAAa,CAAA"}
@@ -0,0 +1,6 @@
1
+ export { getDialect, DUCKDB_DIALECT, SQLITE_DIALECT } from "./dialect.js";
2
+ export { createAdapter } from "./adapter-factory.js";
3
+ // Re-export commonly used functions from db.ts
4
+ export { initDatabase, closeDatabase, querySQL, runSQL, insertRecord, deleteRecord, queryRecords, searchRecords, getRecordByUri, getCursor, setCursor, bulkInsertRecords, packCursor, unpackCursor, } from "./db.js";
5
+ // Re-export schema utilities
6
+ export { loadLexicons, discoverCollections, buildSchemas, generateTableSchema, generateCreateTableSQL, toSnakeCase, getLexicon, getLexiconArray, getAllLexicons, storeLexicons, } from "./schema.js";
@@ -0,0 +1,44 @@
1
+ export type Dialect = 'duckdb' | 'sqlite' | 'postgres';
2
+ export interface DatabasePort {
3
+ /** Dialect identifier for SQL generation differences */
4
+ dialect: Dialect;
5
+ /** Open a database connection. path is file path or ':memory:' */
6
+ open(path: string): Promise<void>;
7
+ /** Close all connections and release resources */
8
+ close(): void;
9
+ /** Execute a read query, return rows as plain objects */
10
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
11
+ /** Execute a write statement (INSERT, UPDATE, DELETE, DDL) */
12
+ execute(sql: string, params?: unknown[]): Promise<void>;
13
+ /** Execute multiple statements in sequence (for DDL batches) */
14
+ executeMultiple(sql: string): Promise<void>;
15
+ /** Begin a transaction */
16
+ beginTransaction(): Promise<void>;
17
+ /** Commit the current transaction */
18
+ commit(): Promise<void>;
19
+ /** Rollback the current transaction */
20
+ rollback(): Promise<void>;
21
+ /** Create a bulk inserter for high-throughput writes */
22
+ createBulkInserter(table: string, columns: string[], options?: {
23
+ onConflict?: 'ignore' | 'replace';
24
+ batchSize?: number;
25
+ }): Promise<BulkInserter>;
26
+ }
27
+ export interface BulkInserter {
28
+ /** Append a single row of values */
29
+ append(values: unknown[]): void;
30
+ /** Flush buffered rows to the database */
31
+ flush(): Promise<void>;
32
+ /** Close the inserter and release resources */
33
+ close(): Promise<void>;
34
+ }
35
+ export interface SearchPort {
36
+ /** Build/rebuild an FTS index for a table */
37
+ buildIndex(shadowTable: string, sourceQuery: string, searchColumns: string[]): Promise<void>;
38
+ /** Search a table, returning URIs with scores */
39
+ search(shadowTable: string, query: string, searchColumns: string[], limit: number, offset: number): Promise<Array<{
40
+ uri: string;
41
+ score: number;
42
+ }>>;
43
+ }
44
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../../src/database/ports.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAA;AAEtD,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,OAAO,EAAE,OAAO,CAAA;IAEhB,kEAAkE;IAClE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEjC,kDAAkD;IAClD,KAAK,IAAI,IAAI,CAAA;IAEb,yDAAyD;IACzD,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAA;IAEjF,8DAA8D;IAC9D,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvD,gEAAgE;IAChE,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE3C,0BAA0B;IAC1B,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEjC,qCAAqC;IACrC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvB,uCAAuC;IACvC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEzB,wDAAwD;IACxD,kBAAkB,CAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAClE,OAAO,CAAC,YAAY,CAAC,CAAA;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAE/B,0CAA0C;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEtB,+CAA+C;IAC/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE5F,iDAAiD;IACjD,MAAM,CACJ,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,EAAE,EACvB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CAClD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import type { SqlDialect } from './dialect.ts';
2
+ export interface ColumnDef {
3
+ name: string;
4
+ originalName: string;
5
+ sqlType: string;
6
+ notNull: boolean;
7
+ isRef: boolean;
8
+ }
9
+ export interface UnionBranchSchema {
10
+ type: string;
11
+ branchName: string;
12
+ tableName: string;
13
+ columns: ColumnDef[];
14
+ isArray: boolean;
15
+ arrayField?: string;
16
+ wrapperField?: string;
17
+ }
18
+ export interface UnionFieldSchema {
19
+ fieldName: string;
20
+ branches: UnionBranchSchema[];
21
+ }
22
+ export interface TableSchema {
23
+ collection: string;
24
+ tableName: string;
25
+ columns: ColumnDef[];
26
+ refColumns: string[];
27
+ children: ChildTableSchema[];
28
+ unions: UnionFieldSchema[];
29
+ }
30
+ export interface ChildTableSchema {
31
+ parentCollection: string;
32
+ fieldName: string;
33
+ tableName: string;
34
+ columns: ColumnDef[];
35
+ }
36
+ export declare function toSnakeCase(str: string): string;
37
+ export declare function loadLexicons(lexiconsDir: string): Map<string, any>;
38
+ /**
39
+ * Discover collections by scanning lexicons for record-type definitions.
40
+ */
41
+ export declare function discoverCollections(lexicons: Map<string, any>): string[];
42
+ export declare function storeLexicons(lexicons: Map<string, any>): void;
43
+ export declare function getLexicon(nsid: string): any | undefined;
44
+ export declare function getAllLexicons(): Array<{
45
+ nsid: string;
46
+ lexicon: any;
47
+ }>;
48
+ /** Get all stored lexicons as a flat array (for @bigmoves/lexicon validators) */
49
+ export declare function getLexiconArray(): any[];
50
+ export declare function generateTableSchema(nsid: string, lexicon: any, lexicons?: Map<string, any>, dialect?: SqlDialect): TableSchema;
51
+ export declare function generateCreateTableSQL(schema: TableSchema, dialect?: SqlDialect): string;
52
+ /**
53
+ * Build table schemas and DDL from lexicons and collections.
54
+ * Shared by main.ts (server boot) and cli.ts (hatk schema command).
55
+ */
56
+ export declare function buildSchemas(lexicons: Map<string, any>, collections: string[], dialect?: SqlDialect): {
57
+ schemas: TableSchema[];
58
+ ddlStatements: string[];
59
+ };
60
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/database/schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAG9C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,iBAAiB,EAAE,CAAA;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,EAAE,gBAAgB,EAAE,CAAA;IAC5B,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;CACrB;AAGD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AA8CD,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CASlE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,CASxE;AAID,wBAAgB,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAI9D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAExD;AAED,wBAAgB,cAAc,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,CAEtE;AAED,iFAAiF;AACjF,wBAAgB,eAAe,IAAI,GAAG,EAAE,CAEvC;AAwHD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,GAAG,EACZ,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,OAAO,GAAE,UAA2B,GACnC,WAAW,CA0Gb;AAGD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,UAA2B,GAAG,MAAM,CAoExG;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE,UAA2B,GACnC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CA2BrD"}
@@ -0,0 +1,388 @@
1
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { DUCKDB_DIALECT } from "./dialect.js";
4
+ // Convert camelCase to snake_case
5
+ export function toSnakeCase(str) {
6
+ return str.replace(/([A-Z])/g, '_$1').toLowerCase();
7
+ }
8
+ function mapType(prop, dialect) {
9
+ if (prop.type === 'string') {
10
+ if (prop.format === 'datetime')
11
+ return { sqlType: dialect.typeMap.timestamp, isRef: false };
12
+ if (prop.format === 'at-uri')
13
+ return { sqlType: dialect.typeMap.text, isRef: true };
14
+ return { sqlType: dialect.typeMap.text, isRef: false };
15
+ }
16
+ if (prop.type === 'integer')
17
+ return { sqlType: dialect.typeMap.integer, isRef: false };
18
+ if (prop.type === 'boolean')
19
+ return { sqlType: dialect.typeMap.boolean, isRef: false };
20
+ if (prop.type === 'bytes')
21
+ return { sqlType: dialect.typeMap.blob, isRef: false };
22
+ if (prop.type === 'cid-link')
23
+ return { sqlType: dialect.typeMap.text, isRef: false };
24
+ if (prop.type === 'array')
25
+ return { sqlType: dialect.jsonType, isRef: false };
26
+ if (prop.type === 'blob')
27
+ return { sqlType: dialect.jsonType, isRef: false };
28
+ if (prop.type === 'union')
29
+ return { sqlType: dialect.jsonType, isRef: false };
30
+ if (prop.type === 'unknown')
31
+ return { sqlType: dialect.jsonType, isRef: false };
32
+ if (prop.type === 'object')
33
+ return { sqlType: dialect.jsonType, isRef: false };
34
+ if (prop.type === 'ref') {
35
+ // strongRef contains { uri, cid } — handled specially in generateTableSchema
36
+ if (prop.ref === 'com.atproto.repo.strongRef')
37
+ return { sqlType: 'STRONG_REF', isRef: true };
38
+ return { sqlType: dialect.jsonType, isRef: false };
39
+ }
40
+ return { sqlType: dialect.typeMap.text, isRef: false };
41
+ }
42
+ // Recursively find all .json files in a directory
43
+ function findJsonFiles(dir) {
44
+ const results = [];
45
+ for (const entry of readdirSync(dir)) {
46
+ const full = join(dir, entry);
47
+ if (statSync(full).isDirectory()) {
48
+ results.push(...findJsonFiles(full));
49
+ }
50
+ else if (entry.endsWith('.json')) {
51
+ results.push(full);
52
+ }
53
+ }
54
+ return results;
55
+ }
56
+ // Load all lexicon files and index by NSID
57
+ export function loadLexicons(lexiconsDir) {
58
+ const lexicons = new Map();
59
+ for (const file of findJsonFiles(lexiconsDir)) {
60
+ const content = JSON.parse(readFileSync(file, 'utf-8'));
61
+ if (content.lexicon === 1 && content.id) {
62
+ lexicons.set(content.id, content);
63
+ }
64
+ }
65
+ return lexicons;
66
+ }
67
+ /**
68
+ * Discover collections by scanning lexicons for record-type definitions.
69
+ */
70
+ export function discoverCollections(lexicons) {
71
+ const collections = [];
72
+ for (const [nsid, lexicon] of lexicons) {
73
+ const mainDef = lexicon.defs?.main;
74
+ if (mainDef?.type === 'record') {
75
+ collections.push(nsid);
76
+ }
77
+ }
78
+ return collections.sort();
79
+ }
80
+ const storedLexicons = new Map();
81
+ export function storeLexicons(lexicons) {
82
+ for (const [nsid, lex] of lexicons) {
83
+ storedLexicons.set(nsid, lex);
84
+ }
85
+ }
86
+ export function getLexicon(nsid) {
87
+ return storedLexicons.get(nsid);
88
+ }
89
+ export function getAllLexicons() {
90
+ return [...storedLexicons.entries()].map(([nsid, lexicon]) => ({ nsid, lexicon }));
91
+ }
92
+ /** Get all stored lexicons as a flat array (for @bigmoves/lexicon validators) */
93
+ export function getLexiconArray() {
94
+ return [...storedLexicons.values()];
95
+ }
96
+ function resolveArrayItemProperties(items, defs) {
97
+ if (!items)
98
+ return null;
99
+ // Inline object with properties
100
+ if (items.type === 'object' && items.properties) {
101
+ return items.properties;
102
+ }
103
+ // Ref to a named def (e.g., "#artist")
104
+ if (items.type === 'ref' && items.ref?.startsWith('#')) {
105
+ const defName = items.ref.slice(1);
106
+ const def = defs?.[defName];
107
+ if (def?.type === 'object' && def.properties) {
108
+ return def.properties;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ /** Resolve a ref string to its definition object */
114
+ function resolveRefDef(ref, defs, lexicons) {
115
+ if (ref.startsWith('#')) {
116
+ return defs?.[ref.slice(1)] || null;
117
+ }
118
+ if (ref.includes('#')) {
119
+ const [nsid, defName] = ref.split('#');
120
+ return lexicons?.get(nsid)?.defs?.[defName] || null;
121
+ }
122
+ return lexicons?.get(ref)?.defs?.main || null;
123
+ }
124
+ /** Resolve a single union ref to a branch schema */
125
+ function resolveUnionBranch(ref, collection, fieldName, defs, lexicons, dialect) {
126
+ let branchDef = null;
127
+ let branchName;
128
+ let fullType;
129
+ let branchDefs = defs; // defs context for resolving inner refs
130
+ if (ref.startsWith('#')) {
131
+ const defName = ref.slice(1);
132
+ branchDef = defs?.[defName];
133
+ branchName = toSnakeCase(defName);
134
+ fullType = `${collection}#${defName}`;
135
+ }
136
+ else if (ref.includes('#')) {
137
+ const [nsid, defName] = ref.split('#');
138
+ const lex = lexicons?.get(nsid);
139
+ branchDef = lex?.defs?.[defName];
140
+ branchName = toSnakeCase(defName);
141
+ fullType = ref;
142
+ branchDefs = lex?.defs || defs;
143
+ }
144
+ else {
145
+ const lex = lexicons?.get(ref);
146
+ branchDef = lex?.defs?.main;
147
+ branchName = ref.split('.').pop();
148
+ fullType = ref;
149
+ branchDefs = lex?.defs || defs;
150
+ }
151
+ if (!branchDef || branchDef.type !== 'object' || !branchDef.properties)
152
+ return null;
153
+ let isArray = false;
154
+ let arrayField;
155
+ let wrapperField;
156
+ let propSource = branchDef.properties;
157
+ const branchRequired = new Set(branchDef.required || []);
158
+ // Check for single-property wrapper patterns
159
+ const propEntries = Object.entries(branchDef.properties);
160
+ if (propEntries.length === 1) {
161
+ const [onlyField, onlyProp] = propEntries[0];
162
+ if (onlyProp.type === 'array' && onlyProp.items) {
163
+ // Single array property (like embed.images wrapping images[])
164
+ const items = onlyProp.items;
165
+ const itemDef = items.type === 'ref' && items.ref ? resolveRefDef(items.ref, branchDefs, lexicons) : items;
166
+ if (itemDef?.type === 'object' && itemDef.properties) {
167
+ isArray = true;
168
+ arrayField = onlyField;
169
+ propSource = itemDef.properties;
170
+ }
171
+ }
172
+ else if (onlyProp.type === 'ref' && onlyProp.ref) {
173
+ // Single ref property (like embed.external wrapping external{})
174
+ const refDef = resolveRefDef(onlyProp.ref, branchDefs, lexicons);
175
+ if (refDef?.type === 'object' && refDef.properties) {
176
+ wrapperField = onlyField;
177
+ propSource = refDef.properties;
178
+ }
179
+ }
180
+ }
181
+ const snakeField = toSnakeCase(fieldName);
182
+ const tableName = `"${collection}__${snakeField}_${branchName}"`;
183
+ const columns = [];
184
+ for (const [propName, prop] of Object.entries(propSource)) {
185
+ const { sqlType, isRef } = mapType(prop, dialect);
186
+ // Skip STRONG_REF expansion in branch tables — treat as JSON
187
+ const finalType = sqlType === 'STRONG_REF' ? dialect.jsonType : sqlType;
188
+ columns.push({
189
+ name: toSnakeCase(propName),
190
+ originalName: propName,
191
+ sqlType: finalType,
192
+ notNull: branchRequired.has(propName),
193
+ isRef: finalType !== 'JSON' && isRef,
194
+ });
195
+ }
196
+ return { type: fullType, branchName, tableName, columns, isArray, arrayField, wrapperField };
197
+ }
198
+ // Generate a TableSchema from a lexicon record definition
199
+ export function generateTableSchema(nsid, lexicon, lexicons, dialect = DUCKDB_DIALECT) {
200
+ const mainDef = lexicon.defs?.main;
201
+ if (!mainDef || mainDef.type !== 'record') {
202
+ throw new Error(`Lexicon ${nsid} does not define a record type`);
203
+ }
204
+ const record = mainDef.record;
205
+ if (!record || record.type !== 'object') {
206
+ throw new Error(`Lexicon ${nsid} record is not an object type`);
207
+ }
208
+ const required = new Set(record.required || []);
209
+ const columns = [];
210
+ const children = [];
211
+ const unions = [];
212
+ for (const [fieldName, prop] of Object.entries(record.properties || {})) {
213
+ const p = prop;
214
+ // Check for union fields — decompose into branch child tables
215
+ if (p.type === 'union' && p.refs) {
216
+ const branches = [];
217
+ for (const ref of p.refs) {
218
+ const branch = resolveUnionBranch(ref, nsid, fieldName, lexicon.defs, lexicons, dialect);
219
+ if (branch)
220
+ branches.push(branch);
221
+ }
222
+ if (branches.length > 0) {
223
+ unions.push({ fieldName, branches });
224
+ }
225
+ // Still add the JSON column for the raw union value
226
+ columns.push({
227
+ name: toSnakeCase(fieldName),
228
+ originalName: fieldName,
229
+ sqlType: dialect.jsonType,
230
+ notNull: required.has(fieldName),
231
+ isRef: false,
232
+ });
233
+ continue;
234
+ }
235
+ // Check if this is a decomposable array (array of structured objects)
236
+ if (p.type === 'array') {
237
+ const itemProps = resolveArrayItemProperties(p.items, lexicon.defs);
238
+ if (itemProps) {
239
+ const childColumns = [];
240
+ const itemRequired = new Set(p.items?.required || lexicon.defs?.[p.items?.ref?.slice(1)]?.required || []);
241
+ for (const [itemField, itemProp] of Object.entries(itemProps)) {
242
+ const { sqlType, isRef } = mapType(itemProp, dialect);
243
+ childColumns.push({
244
+ name: toSnakeCase(itemField),
245
+ originalName: itemField,
246
+ sqlType,
247
+ notNull: itemRequired.has(itemField),
248
+ isRef,
249
+ });
250
+ }
251
+ const snakeField = toSnakeCase(fieldName);
252
+ children.push({
253
+ parentCollection: nsid,
254
+ fieldName,
255
+ tableName: `"${nsid}__${snakeField}"`,
256
+ columns: childColumns,
257
+ });
258
+ continue;
259
+ }
260
+ }
261
+ const { sqlType, isRef } = mapType(p, dialect);
262
+ if (sqlType === 'STRONG_REF') {
263
+ // Expand strongRef into two columns: {name}_uri and {name}_cid
264
+ columns.push({
265
+ name: toSnakeCase(fieldName) + '_uri',
266
+ originalName: fieldName,
267
+ sqlType: dialect.typeMap.text,
268
+ notNull: required.has(fieldName),
269
+ isRef: true,
270
+ });
271
+ columns.push({
272
+ name: toSnakeCase(fieldName) + '_cid',
273
+ originalName: fieldName + '__cid',
274
+ sqlType: dialect.typeMap.text,
275
+ notNull: required.has(fieldName),
276
+ isRef: false,
277
+ });
278
+ }
279
+ else {
280
+ columns.push({
281
+ name: toSnakeCase(fieldName),
282
+ originalName: fieldName,
283
+ sqlType,
284
+ notNull: required.has(fieldName),
285
+ isRef,
286
+ });
287
+ }
288
+ }
289
+ const refColumns = columns.filter((c) => c.isRef).map((c) => c.name);
290
+ return {
291
+ collection: nsid,
292
+ tableName: `"${nsid}"`,
293
+ columns,
294
+ refColumns,
295
+ children,
296
+ unions,
297
+ };
298
+ }
299
+ // Generate CREATE TABLE SQL from a TableSchema
300
+ export function generateCreateTableSQL(schema, dialect = DUCKDB_DIALECT) {
301
+ const lines = [
302
+ ' uri TEXT PRIMARY KEY',
303
+ ' cid TEXT',
304
+ ' did TEXT NOT NULL',
305
+ ` indexed_at ${dialect.timestampType} NOT NULL`,
306
+ ];
307
+ for (const col of schema.columns) {
308
+ const nullable = col.notNull ? ' NOT NULL' : '';
309
+ lines.push(` ${col.name} ${col.sqlType}${nullable}`);
310
+ }
311
+ const createTable = `CREATE TABLE IF NOT EXISTS ${schema.tableName} (\n${lines.join(',\n')}\n);`;
312
+ const prefix = schema.collection.replace(/\./g, '_');
313
+ const indexes = [
314
+ `CREATE INDEX IF NOT EXISTS idx_${prefix}_indexed ON ${schema.tableName}(indexed_at DESC);`,
315
+ `CREATE INDEX IF NOT EXISTS idx_${prefix}_author ON ${schema.tableName}(did);`,
316
+ ];
317
+ // Index ref columns for hydration lookups
318
+ for (const refCol of schema.refColumns) {
319
+ indexes.push(`CREATE INDEX IF NOT EXISTS idx_${prefix}_${refCol} ON ${schema.tableName}(${refCol});`);
320
+ }
321
+ // Child table DDL
322
+ const childDDL = [];
323
+ for (const child of schema.children) {
324
+ const childLines = [' parent_uri TEXT NOT NULL', ' parent_did TEXT NOT NULL'];
325
+ for (const col of child.columns) {
326
+ const nullable = col.notNull ? ' NOT NULL' : '';
327
+ childLines.push(` ${col.name} ${col.sqlType}${nullable}`);
328
+ }
329
+ childDDL.push(`CREATE TABLE IF NOT EXISTS ${child.tableName} (\n${childLines.join(',\n')}\n);`);
330
+ const childPrefix = `${prefix}__${toSnakeCase(child.fieldName)}`;
331
+ childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${childPrefix}_parent ON ${child.tableName}(parent_uri);`);
332
+ childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${childPrefix}_did ON ${child.tableName}(parent_did);`);
333
+ for (const col of child.columns) {
334
+ if (col.sqlType === 'JSON' || col.sqlType === 'BLOB')
335
+ continue;
336
+ childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${childPrefix}_${col.name} ON ${child.tableName}(${col.name});`);
337
+ }
338
+ }
339
+ // Union branch table DDL
340
+ for (const union of schema.unions) {
341
+ for (const branch of union.branches) {
342
+ const branchLines = [' parent_uri TEXT NOT NULL', ' parent_did TEXT NOT NULL'];
343
+ for (const col of branch.columns) {
344
+ const nullable = col.notNull ? ' NOT NULL' : '';
345
+ branchLines.push(` ${col.name} ${col.sqlType}${nullable}`);
346
+ }
347
+ childDDL.push(`CREATE TABLE IF NOT EXISTS ${branch.tableName} (\n${branchLines.join(',\n')}\n);`);
348
+ const branchPrefix = branch.tableName.replace(/"/g, '').replace(/\./g, '_');
349
+ childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${branchPrefix}_parent ON ${branch.tableName}(parent_uri);`);
350
+ childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${branchPrefix}_did ON ${branch.tableName}(parent_did);`);
351
+ for (const col of branch.columns) {
352
+ if (col.sqlType === 'JSON' || col.sqlType === 'BLOB')
353
+ continue;
354
+ childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${branchPrefix}_${col.name} ON ${branch.tableName}(${col.name});`);
355
+ }
356
+ }
357
+ }
358
+ return [createTable, ...indexes, ...childDDL].join('\n');
359
+ }
360
+ /**
361
+ * Build table schemas and DDL from lexicons and collections.
362
+ * Shared by main.ts (server boot) and cli.ts (hatk schema command).
363
+ */
364
+ export function buildSchemas(lexicons, collections, dialect = DUCKDB_DIALECT) {
365
+ const schemas = [];
366
+ const ddlStatements = [];
367
+ for (const nsid of collections) {
368
+ const lexicon = lexicons.get(nsid);
369
+ if (!lexicon) {
370
+ const genericDDL = `CREATE TABLE IF NOT EXISTS "${nsid}" (
371
+ uri TEXT PRIMARY KEY,
372
+ cid TEXT,
373
+ did TEXT NOT NULL,
374
+ indexed_at ${dialect.timestampType} NOT NULL,
375
+ data ${dialect.jsonType}
376
+ );
377
+ CREATE INDEX IF NOT EXISTS idx_${nsid.replace(/\./g, '_')}_indexed ON "${nsid}"(indexed_at DESC);
378
+ CREATE INDEX IF NOT EXISTS idx_${nsid.replace(/\./g, '_')}_author ON "${nsid}"(did);`;
379
+ schemas.push({ collection: nsid, tableName: `"${nsid}"`, columns: [], refColumns: [], children: [], unions: [] });
380
+ ddlStatements.push(genericDDL);
381
+ continue;
382
+ }
383
+ const schema = generateTableSchema(nsid, lexicon, lexicons, dialect);
384
+ schemas.push(schema);
385
+ ddlStatements.push(generateCreateTableSQL(schema, dialect));
386
+ }
387
+ return { schemas, ddlStatements };
388
+ }
package/dist/feeds.js CHANGED
@@ -9,7 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { resolve } from 'node:path';
10
10
  import { readdirSync } from 'node:fs';
11
11
  import { log } from "./logger.js";
12
- import { querySQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids } from "./db.js";
12
+ import { querySQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids } from "./database/db.js";
13
13
  import { resolveRecords, buildHydrateContext } from "./hydrate.js";
14
14
  export function createPaginate(deps) {
15
15
  return async (sql, opts) => {
package/dist/hooks.js CHANGED
@@ -30,7 +30,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
30
30
  import { existsSync } from 'node:fs';
31
31
  import { resolve } from 'node:path';
32
32
  import { log } from "./logger.js";
33
- import { setRepoStatus } from "./db.js";
33
+ import { setRepoStatus } from "./database/db.js";
34
34
  import { triggerAutoBackfill } from "./indexer.js";
35
35
  let onLoginHook = null;
36
36
  /**
package/dist/hydrate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getRecordsByUris, countByFieldBatch, lookupByFieldBatch, querySQL, reshapeRow, queryLabelsForUris, filterTakendownDids, } from "./db.js";
1
+ import { getRecordsByUris, countByFieldBatch, lookupByFieldBatch, querySQL, reshapeRow, queryLabelsForUris, filterTakendownDids, } from "./database/db.js";
2
2
  import { blobUrl } from "./xrpc.js";
3
3
  // --- Record Resolution ---
4
4
  /** Fetch records for URIs, reshape them, and filter out taken-down DIDs. */
@@ -1 +1 @@
1
- {"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"AA2IA;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAgEjF;AAED,8CAA8C;AAC9C,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAyBD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAmDxE"}
1
+ {"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"AAkJA;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAgEjF;AAED,8CAA8C;AAC9C,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAyBD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAmDxE"}
package/dist/indexer.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { cborDecode } from "./cbor.js";
2
2
  import { parseCarFrame } from "./car.js";
3
- import { insertRecord, deleteRecord, setCursor, setRepoStatus, getRepoRetryInfo, listAllRepoStatuses } from "./db.js";
3
+ import { insertRecord, deleteRecord, setCursor, setRepoStatus, getRepoRetryInfo, listAllRepoStatuses, } from "./database/db.js";
4
4
  import { backfillRepo } from "./backfill.js";
5
- import { rebuildAllIndexes } from "./fts.js";
5
+ import { rebuildAllIndexes } from "./database/fts.js";
6
6
  import { log, emit, timer } from "./logger.js";
7
7
  import { runLabelRules } from "./labels.js";
8
- import { getLexiconArray } from "./schema.js";
8
+ import { getLexiconArray } from "./database/schema.js";
9
9
  import { validateRecord } from '@bigmoves/lexicon';
10
10
  let buffer = [];
11
11
  let flushTimer = null;
package/dist/labels.js CHANGED
@@ -36,7 +36,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
36
36
  */
37
37
  import { resolve } from 'node:path';
38
38
  import { readdirSync } from 'node:fs';
39
- import { querySQL, runSQL, insertLabels, getSchema } from "./db.js";
39
+ import { querySQL, runSQL, insertLabels, getSchema } from "./database/db.js";
40
40
  import { log, emit } from "./logger.js";
41
41
  const rules = [];
42
42
  let labelDefs = [];
@@ -132,7 +132,7 @@ export async function rescanLabels(collections) {
132
132
  let v = row[col.name];
133
133
  if (v === null || v === undefined)
134
134
  continue;
135
- if (col.duckdbType === 'JSON' && typeof v === 'string') {
135
+ if (col.sqlType === 'JSON' && typeof v === 'string') {
136
136
  try {
137
137
  v = JSON.parse(v);
138
138
  }