Skip to content

Commit a9c5843

Browse files
committed
Simplify type system
1 parent f6ff8e9 commit a9c5843

File tree

7 files changed

+59
-53
lines changed

7 files changed

+59
-53
lines changed

src/adapter.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Model } from "./model";
2-
import { WithId } from "./types";
2+
import { ModelAttributes, WithId } from "./types";
33

44
export interface SaveResult {
55
success: boolean;
@@ -14,12 +14,12 @@ export interface SaveResult {
1414
* @param C - The context type.
1515
* @param T - The model attributes type.
1616
*/
17-
export type AdapterConfig<C, T> = {
18-
getContext: () => Promise<C>;
19-
all: (context: C, matchOrQuery?: Partial<T> | string, bindValues?: any[]) => Promise<WithId<T>[]>;
20-
get: (context: C, id: any) => Promise<WithId<T> | null>;
21-
getBy: (context: C, matchOrQuery: Partial<T> | string, bindValues?: any[]) => Promise<WithId<T> | null>;
22-
insert: (context: C, data: Partial<T>) => Promise<SaveResult>;
23-
update: (context: C, model: Model<T>, data: Partial<T>) => Promise<SaveResult>;
24-
del: (context: C, model: Model<T>) => Promise<boolean>;
17+
export type AdapterConfig<T extends ModelAttributes> = {
18+
getContext: () => Promise<any>;
19+
all: (context: any, matchOrQuery?: Partial<T> | string, bindValues?: any[]) => Promise<WithId<T>[]>;
20+
get: (context: any, id: any) => Promise<WithId<T> | null>;
21+
getBy: (context: any, matchOrQuery: Partial<T> | string, bindValues?: any[]) => Promise<WithId<T> | null>;
22+
insert: (context: any, data: Partial<T>) => Promise<SaveResult>;
23+
update: (context: any, model: Model<T>, data: Partial<T>) => Promise<SaveResult>;
24+
del: (context: any, model: Model<T>) => Promise<boolean>;
2525
}

src/model.ts

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { PersistenceInfo, WithId } from "./types";
1+
import { ModelAttributes, ModelType, PersistenceInfo, WithId, WithOptionalId } from "./types";
22

33
/**
44
* Base class for all models. Set persistence information using the `Persistence` decorator.
55
*/
6-
export class Model<T, C = any> {
7-
protected static persistence: PersistenceInfo<any, any>;
6+
export class Model<T extends ModelAttributes> {
7+
protected static persistence: PersistenceInfo<Model<any>>;
88
protected data: T = {} as T;
99
protected changedFields: Set<string> = new Set();
1010
protected _persisted: boolean;
@@ -15,8 +15,8 @@ export class Model<T, C = any> {
1515
*
1616
* @returns The persistence information configured for this model.
1717
*/
18-
public static getPersistence<T, C>() {
19-
return this.persistence as PersistenceInfo<T, C>;
18+
public static getPersistence<T extends ModelAttributes>() {
19+
return this.persistence as PersistenceInfo<Model<T>>;
2020
}
2121

2222
/**
@@ -68,7 +68,7 @@ export class Model<T, C = any> {
6868
* @param data - The data to initialize the model with.
6969
* @param persisted - Whether the model is already persisted to the database.
7070
*/
71-
constructor(data: T & { id?: any }, persisted: boolean = false) {
71+
constructor(data: WithOptionalId<T>, persisted: boolean = false) {
7272
// TODO: generate ID using context
7373
this.id = data.id;
7474
const { id, ...rest } = data;
@@ -89,9 +89,9 @@ export class Model<T, C = any> {
8989
* @param value - The value to set the field to, if setting a single field. Ignored otherwise.
9090
* @returns The model instance.
9191
*/
92-
public set<K extends keyof T>(key: K, value: T[K]): Model<T, C>;
93-
public set(changes: Partial<T>): Model<T, C>;
94-
public set<K extends keyof T>(keyOrChanges: K | Partial<T>, value?: T[K]): Model<T, C> {
92+
public set<K extends keyof T>(key: K, value: T[K]): Model<T>;
93+
public set(changes: Partial<T>): Model<T>;
94+
public set<K extends keyof T>(keyOrChanges: K | Partial<T>, value?: T[K]): Model<T> {
9595
if (typeof keyOrChanges === "string") {
9696
this.data[keyOrChanges as K] = value as T[K];
9797
this.changedFields.add(keyOrChanges);
@@ -113,9 +113,9 @@ export class Model<T, C = any> {
113113
* @param value - The value to set the field to, if setting a single field. Ignored otherwise.
114114
* @returns The model instance.
115115
*/
116-
public put<K extends keyof T>(key: K, value: T[K]): Model<T, C>;
117-
public put(changes: Partial<T>): Model<T, C>;
118-
public put<K extends keyof T>(keyOrChanges: K | Partial<T>, value?: T[K]): Model<T, C> {
116+
public put<K extends keyof T>(key: K, value: T[K]): Model<T>;
117+
public put(changes: Partial<T>): Model<T>;
118+
public put<K extends keyof T>(keyOrChanges: K | Partial<T>, value?: T[K]): Model<T> {
119119
if (typeof keyOrChanges === "string") {
120120
this.data[keyOrChanges as K] = value as T[K];
121121
} else {
@@ -142,14 +142,14 @@ export class Model<T, C = any> {
142142
* @param bindValues - Optional array of values to bind to the query if using a SQL string.
143143
* @returns A promise that resolves to an array of matching models.
144144
*/
145-
public static async all<T, M extends Model<T, C>, C = any>(
146-
this: new (...args: any[]) => M & Model<T, C>,
145+
public static async all<T extends ModelAttributes, M extends Model<T>>(
146+
this: new (...args: any[]) => M & Model<T>,
147147
matchOrQuery?: Partial<T> | string,
148148
bindValues?: any[]
149149
): Promise<M[]> {
150-
const { adapter, globalSpec } = (this as unknown as typeof Model<T, C>).getPersistence();
150+
const { adapter, globalSpec } = (this as unknown as typeof Model<T>).getPersistence();
151151
const context = await adapter.getContext();
152-
const rows = await adapter.all(context, matchOrQuery as any, bindValues);
152+
const rows = await adapter.all(context, matchOrQuery, bindValues);
153153
let models = rows.map(row => (this as any).fromRow(row)) as M[];
154154
if (globalSpec?.postLoad) {
155155
const promises = models.map<Promise<M>>(model => globalSpec?.postLoad?.(context, model as any) as Promise<M>);
@@ -164,11 +164,11 @@ export class Model<T, C = any> {
164164
* @param id - The ID of the model to load.
165165
* @returns A promise that resolves to the loaded model, or null if it doesn't exist.
166166
*/
167-
public static async get<T, M extends Model<T, C>, C = any>(
168-
this: new (...args: any[]) => M & Model<T, C>,
167+
public static async get<T extends ModelAttributes, M extends Model<T>>(
168+
this: new (...args: any[]) => M & Model<T>,
169169
id: any
170170
): Promise<M | null> {
171-
const { adapter, globalSpec } = (this as unknown as typeof Model<T, C>).getPersistence();
171+
const { adapter, globalSpec } = (this as unknown as typeof Model<T>).getPersistence();
172172
const context = await adapter.getContext();
173173
const row = await adapter.get(context, id);
174174
if (!row) return null;
@@ -186,12 +186,12 @@ export class Model<T, C = any> {
186186
* @param bindValues - Optional array of values to bind to the query if using a SQL string.
187187
* @returns A promise that resolves to the matching model, or null if none found.
188188
*/
189-
public static async getBy<T, M extends Model<T, C>, C = any>(
190-
this: new (...args: any[]) => M & Model<T, C>,
189+
public static async getBy<T extends ModelAttributes, M extends Model<T>>(
190+
this: new (...args: any[]) => M & Model<T>,
191191
matchOrQuery?: Partial<T> | string,
192192
bindValues?: any[]
193193
): Promise<M | null> {
194-
const { adapter, globalSpec } = (this as unknown as typeof Model<T, C>).getPersistence();
194+
const { adapter, globalSpec } = (this as unknown as typeof Model<T>).getPersistence();
195195
const context = await adapter.getContext();
196196
const row = await adapter.getBy(context, matchOrQuery as any, bindValues);
197197
if (!row) return null;
@@ -208,11 +208,11 @@ export class Model<T, C = any> {
208208
* @param row - The row to create the model from.
209209
* @returns The created model.
210210
*/
211-
protected static fromRow<T, M extends Model<T, C>, C = any>(
212-
this: new (...args: any[]) => M & Model<T, C>,
211+
protected static fromRow<T extends ModelAttributes, M extends Model<T>>(
212+
this: new (...args: any[]) => Model<T>,
213213
row: WithId<T>
214214
): M {
215-
const { fieldSpecs } = (this as unknown as typeof Model<T, C>).getPersistence();
215+
const { fieldSpecs } = (this as unknown as typeof Model<T>).getPersistence();
216216
const data = {} as any;
217217
for (const key in row) {
218218
const fieldSpec = fieldSpecs?.[key];
@@ -222,7 +222,7 @@ export class Model<T, C = any> {
222222
}
223223
}
224224

225-
return new this(data, true);
225+
return new this(data, true) as M;
226226
}
227227

228228
/**
@@ -231,7 +231,7 @@ export class Model<T, C = any> {
231231
* @returns A promise that resolves to the model instance.
232232
*/
233233
public async save(): Promise<this> {
234-
const { adapter, fieldSpecs, globalSpec } = (this.constructor as any).getPersistence() as PersistenceInfo<T, C>;
234+
const { adapter, fieldSpecs, globalSpec } = (this.constructor as any).getPersistence() as PersistenceInfo<Model<T>>;
235235
const fields = this.getChangedFields().filter(field => fieldSpecs?.[field]?.persist !== false);
236236

237237
if (this.persisted && fields.length === 0) return this;

src/persistence.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AdapterConfig } from "./adapter";
22
import { Model } from "./model";
3-
import { FieldSpecs, GlobalSpec, PersistenceInfo } from "./types";
3+
import { FieldSpecs, GlobalSpec, ModelAttributes, PersistenceInfo } from "./types";
44

55
/**
66
* Decorator to add persistence information to a model.
@@ -10,12 +10,12 @@ import { FieldSpecs, GlobalSpec, PersistenceInfo } from "./types";
1010
* @param globalSpec - The `GlobalSpec` to use.
1111
* @returns A decorator function.
1212
*/
13-
export function Persistence<T = any, C = any, M extends Model<T, C> = Model<T, C>>(
14-
adapter: AdapterConfig<C, T>,
13+
export function Persistence<T extends ModelAttributes>(
14+
adapter: AdapterConfig<T>,
1515
fieldSpecs?: FieldSpecs<T>,
16-
globalSpec?: GlobalSpec<T, C, M>
16+
globalSpec?: GlobalSpec<T>
1717
) {
1818
return function (target: any) {
19-
target.persistence = { adapter, fieldSpecs, globalSpec } as PersistenceInfo<T, C, M>;
19+
target.persistence = { adapter, fieldSpecs, globalSpec } as PersistenceInfo<Model<T>>;
2020
}
2121
}

src/types.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { AdapterConfig } from "./adapter";
22
import { Model } from "./model";
33

4+
export type ModelAttributes = Record<any, unknown>;
5+
46
export type WithId<T> = T & { id: any };
7+
export type WithOptionalId<T> = T & { id?: any };
58

69
/**
710
* A `ValueEncoder` is a set of functions that encode and decode values from the database to the model.
@@ -25,15 +28,18 @@ export type FieldSpecs<T> = {
2528
[K in keyof T]?: FieldSpec;
2629
}
2730

28-
export interface GlobalSpec<T, C, M = Model<T, C>> {
29-
preSave?: (context: C, model: M) => Promise<M>;
30-
postSave?: (context: C, model: M) => Promise<M>;
31-
postLoad?: (context: C, model: M) => Promise<M>;
31+
export interface GlobalSpec<T extends ModelAttributes> {
32+
preSave?: (context: any, model: Model<T>) => Promise<Model<T>>;
33+
postSave?: (context: any, model: Model<T>) => Promise<Model<T>>;
34+
postLoad?: (context: any, model: Model<T>) => Promise<Model<T>>;
3235
}
3336

34-
export interface PersistenceInfo<T, C, M extends Model<T, C> = Model<T, C>> {
35-
adapter: AdapterConfig<C, T>;
36-
fieldSpecs?: FieldSpecs<T>;
37-
globalSpec?: GlobalSpec<T, C, M>;
37+
export type ModelType<M> = M extends Model<infer T> ? T : never;
38+
39+
export interface PersistenceInfo<M extends Model<any>> {
40+
adapter: AdapterConfig<ModelType<M>>;
41+
fieldSpecs?: FieldSpecs<ModelType<M>>;
42+
globalSpec?: GlobalSpec<ModelType<M>>;
3843
}
3944

45+

test/advanced-model.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createSqliteAdapter } from "./sqlite-adapter";
33
import { unlink } from 'fs/promises';
44
import { existsSync } from 'fs';
55

6-
interface ComplexAttrs {
6+
type ComplexAttrs = {
77
name: string;
88
metadata: Record<string, any>;
99
secretKey: string;

test/model.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createSqliteAdapter } from "./sqlite-adapter";
33
import { unlink } from 'fs/promises';
44
import { existsSync } from 'fs';
55

6-
interface PersonAttrs {
6+
type PersonAttrs = {
77
firstName: string;
88
lastName: string;
99
age: number;

test/sqlite-adapter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AdapterConfig, Model, WithId } from "../src";
1+
import { AdapterConfig, Model, ModelAttributes, WithId } from "../src";
22
import * as sqlite3 from "sqlite3";
33
import { Database, open } from "sqlite";
44

@@ -29,7 +29,7 @@ function generateId() {
2929
return crypto.randomUUID();
3030
}
3131

32-
export function createSqliteAdapter<T>(dbName: string, tableName: string): AdapterConfig<Context, T> {
32+
export function createSqliteAdapter<T extends ModelAttributes>(dbName: string, tableName: string): AdapterConfig<T> {
3333
let ctx: Context | null = null;
3434

3535
async function getContext() {

0 commit comments

Comments
 (0)