@gobing-ai/ts-db 0.1.7 → 0.2.1

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.
@@ -1,7 +1,8 @@
1
1
  import { type SQL } from 'drizzle-orm';
2
2
  import type { SQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core';
3
- import type { DbClient } from './adapter';
3
+ import type { DbAdapter } from './adapter';
4
4
  import { BaseDao } from './base-dao';
5
+ import { type OrderTerm, type Predicate } from './query-spec';
5
6
  /**
6
7
  * Type for tables compatible with EntityDao.
7
8
  * Must have standard columns: createdAt, updatedAt.
@@ -10,134 +11,136 @@ export type EntityTable = SQLiteTable & {
10
11
  createdAt: SQLiteColumn;
11
12
  updatedAt: SQLiteColumn;
12
13
  };
13
- /**
14
- * Type for tables with soft delete support.
15
- */
14
+ /** Type for tables with soft delete support. */
16
15
  export type SoftDeletableTable = EntityTable & {
17
16
  inUsed: SQLiteColumn;
18
17
  };
19
- /**
20
- * Type for primary key columns.
21
- */
18
+ /** Type for primary key columns (single column or a tuple for composite keys). */
22
19
  export type PKColumn = SQLiteColumn;
20
+ /** A primary key value: a single value, or a tuple for composite keys. */
21
+ export type PKValue = string | number | (string | number)[];
22
+ /** Options for the structured `list` operation. */
23
+ export interface EntityListSpec {
24
+ where?: Predicate;
25
+ orderBy?: readonly OrderTerm[];
26
+ limit?: number;
27
+ offset?: number;
28
+ includeDeleted?: boolean;
29
+ }
30
+ /** Options for keyset/cursor pagination. */
31
+ export interface CursorListSpec {
32
+ /** Opaque cursor from a previous page (the last row's keyset value). */
33
+ cursor?: string | number;
34
+ /** Column the cursor walks (must be unique + ordered, e.g. the primary key). */
35
+ cursorColumn: SQLiteColumn;
36
+ /** Page size. */
37
+ limit: number;
38
+ where?: Predicate;
39
+ direction?: 'asc' | 'desc';
40
+ includeDeleted?: boolean;
41
+ }
23
42
  /**
24
- * Generic CRUD base class for entity DAOs.
43
+ * Generic CRUD base class for entity DAOs — the STRUCTURED tier of the facade.
25
44
  *
26
- * Provides standard create, read, update, delete operations with:
27
- * - Type-safe public API using Drizzle's inference types
28
- * - Automatic soft delete filtering (if table has `inUsed` column)
45
+ * Extends {@link BaseDao} (raw tier) with typed create/read/update/delete over a
46
+ * single table, using RETURNING, drizzle-free predicate filters, soft-delete
47
+ * auto-filtering, batch insert, upsert, and cursor pagination. drizzle is hidden
48
+ * entirely — consumers use ts-db vocabulary. (G1/G3)
29
49
  *
30
- * @typeParam TTable - The table type (must extend EntityTable)
31
- * @typeParam TPK - The primary key column type
50
+ * @typeParam TTable - The table type (must extend EntityTable).
51
+ * @typeParam TPK - The primary key column type.
32
52
  *
33
53
  * @example
34
54
  * ```ts
35
- * export class UsersDao extends EntityDao<typeof users, typeof users.id> {
36
- * constructor(db: DbClient) {
37
- * super(db, users, users.id, 'users');
55
+ * class UsersDao extends EntityDao<typeof users, typeof users.id> {
56
+ * constructor(adapter: DbAdapter) {
57
+ * super(adapter, users, [users.id], 'users');
38
58
  * }
39
- *
40
- * // Add entity-specific methods here
41
- * async findByEmail(email: string) {
59
+ * findByEmail(email: string) {
42
60
  * return this.findBy(users.email, email);
43
61
  * }
44
62
  * }
45
63
  * ```
46
64
  */
65
+ /**
66
+ * A structural validator (e.g. a zod schema). Kept structural so EntityDao never
67
+ * imports zod/drizzle-zod — consumers wire `defineTable().insertSchema` here when
68
+ * they want boundary validation.
69
+ */
70
+ export interface DaoValidator {
71
+ parse(input: unknown): unknown;
72
+ }
73
+ /** Optional EntityDao configuration. */
74
+ export interface EntityDaoOptions {
75
+ /** Schema used to validate input before `create`/`createMany`/`upsert`. */
76
+ insertSchema?: DaoValidator;
77
+ /** Schema used to validate input before `update`. Falls back to `insertSchema` when absent. */
78
+ updateSchema?: DaoValidator;
79
+ /** Which write operations validate. Default: all (`create`, `createMany`, `upsert`, `update`) when a schema is present. */
80
+ validateOn?: ('create' | 'createMany' | 'upsert' | 'update')[];
81
+ }
47
82
  export declare class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> extends BaseDao {
48
83
  readonly table: TTable;
49
- protected readonly primaryKey: TPK;
84
+ /** Primary key columns. A single-element array for single-PK tables, multiple for composite. */
85
+ protected readonly primaryKey: SQLiteColumn[];
50
86
  protected readonly collectionName: string;
51
- constructor(db: DbClient, table: TTable, primaryKey: TPK, collectionName: string);
52
- /**
53
- * Check if the table has soft delete support (inUsed column).
54
- */
87
+ private readonly validation;
88
+ constructor(adapter: DbAdapter, table: TTable, primaryKey: TPK | SQLiteColumn[], collectionName: string, options?: EntityDaoOptions);
89
+ /** Validate input against the configured schema for an operation, when enabled. */
90
+ private validate;
91
+ /** Check if the table has soft delete support (inUsed column). */
55
92
  protected get hasSoftDelete(): boolean;
56
- /**
57
- * Build a where condition that filters out soft-deleted records.
58
- * Returns undefined if the table doesn't support soft delete.
59
- */
93
+ /** Condition filtering out soft-deleted rows, or undefined when unsupported. */
60
94
  protected get activeCondition(): SQL | undefined;
95
+ /** Build the where-condition matching a primary key value (single or composite). */
96
+ private pkCondition;
97
+ private get insertBuilder();
61
98
  /**
62
- * Create a new record.
63
- *
64
- * `createdAt` and `updatedAt` are auto-filled if not provided.
65
- *
66
- * @param data - The data to insert (createdAt/updatedAt optional).
67
- * @returns The created record.
99
+ * Create a new record. `createdAt`/`updatedAt` are auto-filled if absent.
100
+ * Returns the row as written by the database (RETURNING).
68
101
  */
69
102
  create(data: Omit<TTable['$inferInsert'], 'createdAt' | 'updatedAt'> & {
70
103
  createdAt?: number;
71
104
  updatedAt?: number;
72
105
  }): Promise<TTable['$inferSelect']>;
73
106
  /**
74
- * Find a record by its primary key.
75
- *
76
- * @param id - The primary key value.
77
- * @param includeDeleted - Whether to include soft-deleted records.
78
- * @returns The record if found, otherwise undefined.
107
+ * Insert many records in a single multi-VALUES statement (efficient for ETL).
108
+ * Returns the written rows (RETURNING).
79
109
  */
80
- findById(id: string | number, includeDeleted?: boolean): Promise<TTable['$inferSelect'] | undefined>;
110
+ createMany(data: (Omit<TTable['$inferInsert'], 'createdAt' | 'updatedAt'> & {
111
+ createdAt?: number;
112
+ updatedAt?: number;
113
+ })[]): Promise<TTable['$inferSelect'][]>;
81
114
  /**
82
- * Find all records.
83
- *
84
- * @param includeDeleted - Whether to include soft-deleted records.
85
- * @returns Array of records.
115
+ * Insert or update on conflict. `conflictColumns` identify the unique target;
116
+ * `update` (or all non-conflict columns) are written on conflict. Returns the row.
86
117
  */
118
+ upsert(data: Omit<TTable['$inferInsert'], 'createdAt' | 'updatedAt'> & {
119
+ createdAt?: number;
120
+ updatedAt?: number;
121
+ }, conflictColumns: SQLiteColumn[], updateColumns?: Partial<TTable['$inferInsert']>): Promise<TTable['$inferSelect']>;
122
+ /** Find a record by its primary key (single value or composite tuple). */
123
+ findById(id: PKValue, includeDeleted?: boolean): Promise<TTable['$inferSelect'] | undefined>;
124
+ /** Find all records (optionally including soft-deleted). */
87
125
  findAll(includeDeleted?: boolean): Promise<TTable['$inferSelect'][]>;
88
- /**
89
- * Update a record by its primary key.
90
- *
91
- * @param id - The primary key value.
92
- * @param data - The data to update.
93
- * @returns The updated record if found, otherwise undefined.
94
- */
95
- update(id: string | number, data: Partial<TTable['$inferInsert']>): Promise<TTable['$inferSelect'] | undefined>;
96
- /**
97
- * Delete a record by its primary key.
98
- *
99
- * @param id - The primary key value.
100
- * @param soft - Whether to perform a soft delete (default: true if table supports it).
101
- * @returns The deleted record (for soft delete), otherwise undefined.
102
- */
103
- delete(id: string | number, soft?: boolean): Promise<TTable['$inferSelect'] | undefined>;
104
- /**
105
- * Find a record by a specific column value.
106
- *
107
- * @param column - The column to search.
108
- * @param value - The value to match.
109
- * @param includeDeleted - Whether to include soft-deleted records.
110
- * @returns The record if found, otherwise undefined.
111
- */
126
+ /** Find the first record matching a column value. */
112
127
  findBy<TCol extends SQLiteColumn>(column: TCol, value: TCol['_']['data'], includeDeleted?: boolean): Promise<TTable['$inferSelect'] | undefined>;
113
- /**
114
- * Find all records matching a specific column value.
115
- *
116
- * @param column - The column to search.
117
- * @param value - The value to match.
118
- * @param includeDeleted - Whether to include soft-deleted records.
119
- * @returns Array of matching records.
120
- */
128
+ /** Find all records matching a column value. */
121
129
  findAllBy<TCol extends SQLiteColumn>(column: TCol, value: TCol['_']['data'], includeDeleted?: boolean): Promise<TTable['$inferSelect'][]>;
122
- /**
123
- * List records with pagination and optional filtering.
124
- *
125
- * @param options - List options (limit, offset, where).
126
- * @returns Array of records.
127
- */
128
- list(options?: {
129
- limit?: number;
130
- offset?: number;
131
- where?: SQL;
132
- includeDeleted?: boolean;
133
- }): Promise<TTable['$inferSelect'][]>;
134
- /**
135
- * Count records in the table.
136
- *
137
- * @param where - Optional filter condition.
138
- * @param includeDeleted - Whether to include soft-deleted records.
139
- * @returns The count of matching records.
140
- */
141
- count(where?: SQL, includeDeleted?: boolean): Promise<number>;
130
+ /** Update a record by primary key. `updatedAt` is refreshed. Returns the updated row. */
131
+ update(id: PKValue, data: Partial<TTable['$inferInsert']>): Promise<TTable['$inferSelect'] | undefined>;
132
+ /** Delete a record by primary key. Soft-deletes when the table supports it (default). */
133
+ delete(id: PKValue, soft?: boolean): Promise<TTable['$inferSelect'] | undefined>;
134
+ /** List records with predicate filter, ordering, and offset pagination. */
135
+ list(spec?: EntityListSpec): Promise<TTable['$inferSelect'][]>;
136
+ /** List one keyset page; returns the rows and the cursor for the next page. */
137
+ listByCursor(spec: CursorListSpec): Promise<{
138
+ rows: TTable['$inferSelect'][];
139
+ nextCursor?: string | number;
140
+ }>;
141
+ /** Count records matching an optional predicate. */
142
+ count(where?: Predicate, includeDeleted?: boolean): Promise<number>;
143
+ /** Combine a caller predicate with the soft-delete active filter. */
144
+ private withActive;
142
145
  }
143
146
  //# sourceMappingURL=entity-dao.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-dao.d.ts","sourceRoot":"","sources":["../src/entity-dao.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"entity-dao.d.ts","sourceRoot":"","sources":["../src/entity-dao.ts"],"names":[],"mappings":"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"}
@@ -1,218 +1,193 @@
1
1
  import { and, count as countFn, eq } from 'drizzle-orm';
2
2
  import { BaseDao } from './base-dao.js';
3
- /**
4
- * Generic CRUD base class for entity DAOs.
5
- *
6
- * Provides standard create, read, update, delete operations with:
7
- * - Type-safe public API using Drizzle's inference types
8
- * - Automatic soft delete filtering (if table has `inUsed` column)
9
- *
10
- * @typeParam TTable - The table type (must extend EntityTable)
11
- * @typeParam TPK - The primary key column type
12
- *
13
- * @example
14
- * ```ts
15
- * export class UsersDao extends EntityDao<typeof users, typeof users.id> {
16
- * constructor(db: DbClient) {
17
- * super(db, users, users.id, 'users');
18
- * }
19
- *
20
- * // Add entity-specific methods here
21
- * async findByEmail(email: string) {
22
- * return this.findBy(users.email, email);
23
- * }
24
- * }
25
- * ```
26
- */
3
+ import { compilePredicate } from './query-spec.js';
27
4
  export class EntityDao extends BaseDao {
28
5
  table;
6
+ /** Primary key columns. A single-element array for single-PK tables, multiple for composite. */
29
7
  primaryKey;
30
8
  collectionName;
31
- constructor(db, table, primaryKey, collectionName) {
32
- super(db);
9
+ validation;
10
+ constructor(adapter, table, primaryKey, collectionName, options = {}) {
11
+ super(adapter);
33
12
  this.table = table;
34
- this.primaryKey = primaryKey;
13
+ this.primaryKey = Array.isArray(primaryKey) ? primaryKey : [primaryKey];
35
14
  this.collectionName = collectionName;
15
+ this.validation = options;
36
16
  }
37
- /**
38
- * Check if the table has soft delete support (inUsed column).
39
- */
17
+ /** Validate input against the configured schema for an operation, when enabled. */
18
+ validate(op, input) {
19
+ // `update` is partial, so it only validates against an explicit partial
20
+ // `updateSchema`; the full insertSchema would reject a partial payload.
21
+ const schema = op === 'update' ? this.validation.updateSchema : this.validation.insertSchema;
22
+ if (schema === undefined)
23
+ return;
24
+ const enabled = this.validation.validateOn ?? ['create', 'createMany', 'upsert', 'update'];
25
+ if (!enabled.includes(op))
26
+ return;
27
+ schema.parse(input);
28
+ }
29
+ /** Check if the table has soft delete support (inUsed column). */
40
30
  get hasSoftDelete() {
41
31
  return 'inUsed' in this.table;
42
32
  }
43
- /**
44
- * Build a where condition that filters out soft-deleted records.
45
- * Returns undefined if the table doesn't support soft delete.
46
- */
33
+ /** Condition filtering out soft-deleted rows, or undefined when unsupported. */
47
34
  get activeCondition() {
48
35
  if (this.hasSoftDelete) {
49
36
  return eq(this.table.inUsed, 1);
50
37
  }
51
38
  return undefined;
52
39
  }
40
+ /** Build the where-condition matching a primary key value (single or composite). */
41
+ pkCondition(id) {
42
+ const values = Array.isArray(id) ? id : [id];
43
+ if (values.length !== this.primaryKey.length) {
44
+ throw new Error(`${this.collectionName}: primary key expects ${this.primaryKey.length} value(s), got ${values.length}`);
45
+ }
46
+ const parts = this.primaryKey.map((col, i) => eq(col, values[i]));
47
+ return parts.length === 1 ? parts[0] : and(...parts);
48
+ }
49
+ get insertBuilder() {
50
+ return this.db.insert(this.table);
51
+ }
53
52
  /**
54
- * Create a new record.
55
- *
56
- * `createdAt` and `updatedAt` are auto-filled if not provided.
57
- *
58
- * @param data - The data to insert (createdAt/updatedAt optional).
59
- * @returns The created record.
53
+ * Create a new record. `createdAt`/`updatedAt` are auto-filled if absent.
54
+ * Returns the row as written by the database (RETURNING).
60
55
  */
61
56
  async create(data) {
62
57
  const now = this.now();
63
- const record = {
64
- ...data,
65
- createdAt: data.createdAt ?? now,
66
- updatedAt: data.updatedAt ?? now,
67
- };
68
- await this.db.insert(this.table).values(record);
69
- return record;
58
+ const record = { createdAt: now, updatedAt: now, ...data };
59
+ this.validate('create', record);
60
+ const rows = (await this.insertBuilder.values(record).returning());
61
+ return rows[0];
70
62
  }
71
63
  /**
72
- * Find a record by its primary key.
73
- *
74
- * @param id - The primary key value.
75
- * @param includeDeleted - Whether to include soft-deleted records.
76
- * @returns The record if found, otherwise undefined.
64
+ * Insert many records in a single multi-VALUES statement (efficient for ETL).
65
+ * Returns the written rows (RETURNING).
77
66
  */
67
+ async createMany(data) {
68
+ if (data.length === 0)
69
+ return [];
70
+ const now = this.now();
71
+ const records = data.map((d) => ({ createdAt: now, updatedAt: now, ...d }));
72
+ for (const record of records)
73
+ this.validate('createMany', record);
74
+ return (await this.insertBuilder.values(records).returning());
75
+ }
76
+ /**
77
+ * Insert or update on conflict. `conflictColumns` identify the unique target;
78
+ * `update` (or all non-conflict columns) are written on conflict. Returns the row.
79
+ */
80
+ async upsert(data, conflictColumns, updateColumns) {
81
+ const now = this.now();
82
+ const record = { createdAt: now, updatedAt: now, ...data };
83
+ this.validate('upsert', record);
84
+ // Default conflict-update = the supplied data minus identity columns
85
+ // (conflict target + primary key), so a conflict updates the row in place
86
+ // without rewriting the key it matched on. Identity columns are matched by
87
+ // their table property key (not DB column name) to handle snake_case columns.
88
+ const identityCols = new Set([...conflictColumns, ...this.primaryKey]);
89
+ const identityProps = new Set(Object.entries(this.table)
90
+ .filter(([, col]) => identityCols.has(col))
91
+ .map(([key]) => key));
92
+ const defaultSet = Object.fromEntries(Object.entries(data).filter(([key]) => !identityProps.has(key)));
93
+ const setOnConflict = { ...(updateColumns ?? defaultSet), updatedAt: now };
94
+ const rows = (await this.insertBuilder.values(record)
95
+ .onConflictDoUpdate({ target: conflictColumns, set: setOnConflict })
96
+ .returning());
97
+ return rows[0];
98
+ }
99
+ /** Find a record by its primary key (single value or composite tuple). */
78
100
  async findById(id, includeDeleted = false) {
79
- const conditions = [eq(this.primaryKey, id)];
80
- if (!includeDeleted && this.activeCondition) {
101
+ const conditions = [this.pkCondition(id)];
102
+ if (!includeDeleted && this.activeCondition)
81
103
  conditions.push(this.activeCondition);
82
- }
83
- const result = await this.db
104
+ const result = (await this.db
84
105
  .select()
85
106
  .from(this.table)
86
- .where(and(...conditions));
107
+ .where(and(...conditions)));
87
108
  return result[0];
88
109
  }
89
- /**
90
- * Find all records.
91
- *
92
- * @param includeDeleted - Whether to include soft-deleted records.
93
- * @returns Array of records.
94
- */
110
+ /** Find all records (optionally including soft-deleted). */
95
111
  async findAll(includeDeleted = false) {
96
- const query = this.db.select().from(this.table);
97
- if (!includeDeleted && this.activeCondition) {
98
- return query.where(this.activeCondition);
99
- }
100
- return query;
112
+ return this.list({ includeDeleted });
101
113
  }
102
- /**
103
- * Update a record by its primary key.
104
- *
105
- * @param id - The primary key value.
106
- * @param data - The data to update.
107
- * @returns The updated record if found, otherwise undefined.
108
- */
114
+ /** Find the first record matching a column value. */
115
+ async findBy(column, value, includeDeleted = false) {
116
+ const rows = await this.list({ where: { col: column, op: 'eq', value }, limit: 1, includeDeleted });
117
+ return rows[0];
118
+ }
119
+ /** Find all records matching a column value. */
120
+ async findAllBy(column, value, includeDeleted = false) {
121
+ return this.list({ where: { col: column, op: 'eq', value }, includeDeleted });
122
+ }
123
+ /** Update a record by primary key. `updatedAt` is refreshed. Returns the updated row. */
109
124
  async update(id, data) {
110
- const now = this.now();
111
- const updateData = {
112
- ...data,
113
- updatedAt: now,
114
- };
115
- await this.db.update(this.table).set(updateData).where(eq(this.primaryKey, id));
116
- return this.findById(id);
125
+ const updateData = { ...data, updatedAt: this.now() };
126
+ this.validate('update', updateData);
127
+ const rows = (await this.db
128
+ .update(this.table)
129
+ .set(updateData)
130
+ .where(this.pkCondition(id))
131
+ .returning());
132
+ return rows[0];
117
133
  }
118
- /**
119
- * Delete a record by its primary key.
120
- *
121
- * @param id - The primary key value.
122
- * @param soft - Whether to perform a soft delete (default: true if table supports it).
123
- * @returns The deleted record (for soft delete), otherwise undefined.
124
- */
134
+ /** Delete a record by primary key. Soft-deletes when the table supports it (default). */
125
135
  async delete(id, soft) {
126
136
  const useSoftDelete = soft ?? this.hasSoftDelete;
127
137
  if (useSoftDelete && this.hasSoftDelete) {
128
- const now = this.now();
129
- await this.db
130
- .update(this.table)
131
- .set({ inUsed: 0, updatedAt: now })
132
- .where(eq(this.primaryKey, id));
133
- return this.findById(id, true);
138
+ return this.update(id, { inUsed: 0 });
134
139
  }
135
- await this.db.delete(this.table).where(eq(this.primaryKey, id));
140
+ await this.db.delete(this.table).where(this.pkCondition(id));
136
141
  return undefined;
137
142
  }
138
- /**
139
- * Find a record by a specific column value.
140
- *
141
- * @param column - The column to search.
142
- * @param value - The value to match.
143
- * @param includeDeleted - Whether to include soft-deleted records.
144
- * @returns The record if found, otherwise undefined.
145
- */
146
- async findBy(column, value, includeDeleted = false) {
147
- const conditions = [eq(column, value)];
148
- if (!includeDeleted && this.activeCondition) {
149
- conditions.push(this.activeCondition);
150
- }
151
- const result = await this.db
152
- .select()
153
- .from(this.table)
154
- .where(and(...conditions));
155
- return result[0];
143
+ /** List records with predicate filter, ordering, and offset pagination. */
144
+ async list(spec = {}) {
145
+ const where = this.withActive(spec.where, spec.includeDeleted);
146
+ return this.query(this.table, {
147
+ ...(where ? { where } : {}),
148
+ ...(spec.orderBy ? { orderBy: spec.orderBy } : {}),
149
+ ...(spec.limit !== undefined ? { limit: spec.limit } : {}),
150
+ ...(spec.offset !== undefined ? { offset: spec.offset } : {}),
151
+ });
156
152
  }
157
- /**
158
- * Find all records matching a specific column value.
159
- *
160
- * @param column - The column to search.
161
- * @param value - The value to match.
162
- * @param includeDeleted - Whether to include soft-deleted records.
163
- * @returns Array of matching records.
164
- */
165
- async findAllBy(column, value, includeDeleted = false) {
166
- const conditions = [eq(column, value)];
167
- if (!includeDeleted && this.activeCondition) {
168
- conditions.push(this.activeCondition);
153
+ /** List one keyset page; returns the rows and the cursor for the next page. */
154
+ async listByCursor(spec) {
155
+ const dir = spec.direction ?? 'asc';
156
+ const filters = [];
157
+ if (spec.where)
158
+ filters.push(spec.where);
159
+ if (spec.cursor !== undefined) {
160
+ filters.push({ col: spec.cursorColumn, op: dir === 'asc' ? 'gt' : 'lt', value: spec.cursor });
169
161
  }
170
- return this.db
171
- .select()
172
- .from(this.table)
173
- .where(and(...conditions));
162
+ const where = this.withActive(filters.length > 0 ? { and: filters } : undefined, spec.includeDeleted);
163
+ const rows = await this.query(this.table, {
164
+ ...(where ? { where } : {}),
165
+ orderBy: [{ col: spec.cursorColumn, dir }],
166
+ limit: spec.limit,
167
+ });
168
+ const last = rows[rows.length - 1];
169
+ const nextCursor = rows.length === spec.limit && last
170
+ ? last[spec.cursorColumn.name]
171
+ : undefined;
172
+ return nextCursor !== undefined ? { rows, nextCursor } : { rows };
174
173
  }
175
- /**
176
- * List records with pagination and optional filtering.
177
- *
178
- * @param options - List options (limit, offset, where).
179
- * @returns Array of records.
180
- */
181
- async list(options = {}) {
182
- const { limit = 100, offset = 0, where, includeDeleted = false } = options;
183
- const conditions = [];
184
- if (!includeDeleted && this.activeCondition) {
185
- conditions.push(this.activeCondition);
186
- }
187
- if (where) {
188
- conditions.push(where);
189
- }
190
- const query = this.db.select().from(this.table);
191
- if (conditions.length > 0) {
192
- return query
193
- .where(and(...conditions))
194
- .limit(limit)
195
- .offset(offset);
196
- }
197
- return query.limit(limit).offset(offset);
198
- }
199
- /**
200
- * Count records in the table.
201
- *
202
- * @param where - Optional filter condition.
203
- * @param includeDeleted - Whether to include soft-deleted records.
204
- * @returns The count of matching records.
205
- */
174
+ /** Count records matching an optional predicate. */
206
175
  async count(where, includeDeleted = false) {
207
- const conditions = [];
208
- if (!includeDeleted && this.activeCondition) {
209
- conditions.push(this.activeCondition);
210
- }
211
- if (where) {
212
- conditions.push(where);
213
- }
214
- const query = this.db.select({ value: countFn() }).from(this.table);
215
- const result = conditions.length > 0 ? await query.where(and(...conditions)) : await query;
176
+ const condition = this.withActive(where, includeDeleted);
177
+ const compiled = condition ? compilePredicate(condition) : undefined;
178
+ const base = this.db
179
+ .select({ value: countFn() })
180
+ .from(this.table);
181
+ const result = (await (compiled ? base.where(compiled) : base));
216
182
  return result[0]?.value ?? 0;
217
183
  }
184
+ /** Combine a caller predicate with the soft-delete active filter. */
185
+ withActive(where, includeDeleted) {
186
+ if (includeDeleted || !this.hasSoftDelete)
187
+ return where;
188
+ const active = { col: this.table.inUsed, op: 'eq', value: 1 };
189
+ if (!where)
190
+ return active;
191
+ return { and: [where, active] };
192
+ }
218
193
  }
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- export { createDbAdapter, type DbAdapter, type DbAdapterConfig, type DbClient, type DbTable } from './adapter';
1
+ export { createDbAdapter, type DbAdapter, type DbAdapterConfig, type InternalDb } from './adapter';
2
2
  export { BunSqliteAdapter, type BunSqliteOptions } from './adapters/bun-sqlite';
3
3
  export { D1Adapter } from './adapters/d1';
4
- export { BaseDao } from './base-dao';
4
+ export { BaseDao, type TxHandle } from './base-dao';
5
+ export { type DefinedTable, defineTable } from './define-table';
5
6
  export { type EmbeddedMigration, embeddedMigrations } from './embedded-migrations';
6
- export { EntityDao, type EntityTable, type PKColumn, type SoftDeletableTable } from './entity-dao';
7
+ export { type CursorListSpec, type DaoValidator, EntityDao, type EntityDaoOptions, type EntityListSpec, type EntityTable, type PKColumn, type PKValue, type SoftDeletableTable, } from './entity-dao';
7
8
  export { applyMigrations, type MigrationOptions } from './migrate';
9
+ export { type ColRef, type ComparisonOp, compileOrderBy, compilePredicate, type ListSpec, type OrderTerm, type Predicate, } from './query-spec';
8
10
  export { QueueJobDao, type QueueJobRecord, type QueueStats } from './queue-job-dao';
9
11
  export { appendOnlyColumns, buildAppendOnlyColumns, buildStandardColumns, buildStandardColumnsWithSoftDelete, nowTimestamp, standardColumns, standardColumnsWithSoftDelete, } from './schema/common';
10
12
  export { queueJobs } from './schema/queue-jobs';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAC/G,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACnG,OAAO,EAAE,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EACH,iBAAiB,EACjB,sBAAsB,EACtB,oBAAoB,EACpB,kCAAkC,EAClC,YAAY,EACZ,eAAe,EACf,6BAA6B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACnG,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,kBAAkB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EACH,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,cAAc,EACd,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,SAAS,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EACH,iBAAiB,EACjB,sBAAsB,EACtB,oBAAoB,EACpB,kCAAkC,EAClC,YAAY,EACZ,eAAe,EACf,6BAA6B,GAChC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}