@apisr/drizzle-model 2.0.3 → 2.0.4
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.
- package/CHANGELOG.md +4 -0
- package/dist/core/runtime.mjs +2 -2
- package/dist/model/builder.mjs +48 -22
- package/package.json +1 -1
- package/src/core/runtime.ts +1 -1
- package/src/model/builder.ts +73 -30
- package/tests/base/custom-methods.test.ts +335 -0
package/CHANGELOG.md
CHANGED
package/dist/core/runtime.mjs
CHANGED
|
@@ -111,8 +111,8 @@ var ModelRuntime = class ModelRuntime {
|
|
|
111
111
|
...this.config.options,
|
|
112
112
|
...nextOptions,
|
|
113
113
|
methods: {
|
|
114
|
-
...
|
|
115
|
-
...
|
|
114
|
+
...this.config.options.methods ?? {},
|
|
115
|
+
...nextOptions.methods ?? {}
|
|
116
116
|
},
|
|
117
117
|
format: nextOptions.format ?? this.config.options.format
|
|
118
118
|
}
|
package/dist/model/builder.mjs
CHANGED
|
@@ -1,36 +1,62 @@
|
|
|
1
1
|
import { ModelRuntime } from "../core/runtime.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/model/builder.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a {@link ModelRuntime} instance into a plain target object that:
|
|
6
|
+
*
|
|
7
|
+
* 1. Exposes all runtime prototype methods, intercepting any that return a
|
|
8
|
+
* `ModelRuntime` so the result is re-wrapped (preserving custom methods).
|
|
9
|
+
* 2. Defines the `$model`, `$modelName`, and `$format` identity getters.
|
|
10
|
+
* 3. Attaches user-defined custom methods from `options.methods` via
|
|
11
|
+
* `runtime.attachMethods`, binding each to the new `target`.
|
|
12
|
+
*
|
|
13
|
+
* This must be called every time a new `ModelRuntime` is produced (e.g. by
|
|
14
|
+
* `.where()`, `.extend()`, `.db()`) so that custom methods are never lost.
|
|
15
|
+
*/
|
|
16
|
+
function buildTarget(runtime) {
|
|
17
|
+
const target = {};
|
|
18
|
+
for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(runtime))) {
|
|
19
|
+
if (key === "constructor") continue;
|
|
20
|
+
const value = runtime[key];
|
|
21
|
+
if (typeof value === "function") target[key] = (...args) => {
|
|
22
|
+
const result = value.apply(runtime, args);
|
|
23
|
+
if (result instanceof ModelRuntime) return buildTarget(result);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(target, "$model", {
|
|
28
|
+
get: () => runtime.$model,
|
|
29
|
+
enumerable: true
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(target, "$modelName", {
|
|
32
|
+
get: () => runtime.$modelName,
|
|
33
|
+
enumerable: true
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(target, "$format", {
|
|
36
|
+
get: () => runtime.$format,
|
|
37
|
+
enumerable: true
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(target, "$where", {
|
|
40
|
+
get: () => runtime.$where,
|
|
41
|
+
enumerable: true
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(target, "$tableName", {
|
|
44
|
+
get: () => runtime.$tableName,
|
|
45
|
+
enumerable: true
|
|
46
|
+
});
|
|
47
|
+
runtime.attachMethods(target);
|
|
48
|
+
return target;
|
|
49
|
+
}
|
|
4
50
|
function modelBuilder({ db, relations, schema, dialect }) {
|
|
5
51
|
return (table, options) => {
|
|
6
|
-
|
|
52
|
+
return buildTarget(new ModelRuntime({
|
|
7
53
|
db,
|
|
8
54
|
relations,
|
|
9
55
|
schema,
|
|
10
56
|
tableName: table,
|
|
11
57
|
dialect,
|
|
12
58
|
options: options ?? {}
|
|
13
|
-
});
|
|
14
|
-
const target = {};
|
|
15
|
-
for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(runtime))) {
|
|
16
|
-
if (key === "constructor") continue;
|
|
17
|
-
const value = runtime[key];
|
|
18
|
-
if (typeof value === "function") target[key] = value.bind(runtime);
|
|
19
|
-
}
|
|
20
|
-
Object.defineProperty(target, "$model", {
|
|
21
|
-
get: () => runtime.$model,
|
|
22
|
-
enumerable: true
|
|
23
|
-
});
|
|
24
|
-
Object.defineProperty(target, "$modelName", {
|
|
25
|
-
get: () => runtime.$modelName,
|
|
26
|
-
enumerable: true
|
|
27
|
-
});
|
|
28
|
-
Object.defineProperty(target, "$format", {
|
|
29
|
-
get: () => runtime.$format,
|
|
30
|
-
enumerable: true
|
|
31
|
-
});
|
|
32
|
-
runtime.attachMethods(target);
|
|
33
|
-
return target;
|
|
59
|
+
}));
|
|
34
60
|
};
|
|
35
61
|
}
|
|
36
62
|
|
package/package.json
CHANGED
package/src/core/runtime.ts
CHANGED
|
@@ -177,8 +177,8 @@ export class ModelRuntime {
|
|
|
177
177
|
...this.config.options,
|
|
178
178
|
...nextOptions,
|
|
179
179
|
methods: {
|
|
180
|
-
...(nextOptions.methods ?? {}),
|
|
181
180
|
...(this.config.options.methods ?? {}),
|
|
181
|
+
...(nextOptions.methods ?? {}),
|
|
182
182
|
},
|
|
183
183
|
format: nextOptions.format ?? this.config.options.format,
|
|
184
184
|
} as ModelOptions<never, never, never, never>,
|
package/src/model/builder.ts
CHANGED
|
@@ -5,6 +5,78 @@ import type { ModelDialect } from "./dialect.ts";
|
|
|
5
5
|
import type { Model } from "./model.ts";
|
|
6
6
|
import type { ModelOptions } from "./options.ts";
|
|
7
7
|
|
|
8
|
+
type AnyRecord = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wraps a {@link ModelRuntime} instance into a plain target object that:
|
|
12
|
+
*
|
|
13
|
+
* 1. Exposes all runtime prototype methods, intercepting any that return a
|
|
14
|
+
* `ModelRuntime` so the result is re-wrapped (preserving custom methods).
|
|
15
|
+
* 2. Defines the `$model`, `$modelName`, and `$format` identity getters.
|
|
16
|
+
* 3. Attaches user-defined custom methods from `options.methods` via
|
|
17
|
+
* `runtime.attachMethods`, binding each to the new `target`.
|
|
18
|
+
*
|
|
19
|
+
* This must be called every time a new `ModelRuntime` is produced (e.g. by
|
|
20
|
+
* `.where()`, `.extend()`, `.db()`) so that custom methods are never lost.
|
|
21
|
+
*/
|
|
22
|
+
function buildTarget(runtime: ModelRuntime): AnyRecord {
|
|
23
|
+
const target: AnyRecord = {};
|
|
24
|
+
|
|
25
|
+
for (const key of Object.getOwnPropertyNames(
|
|
26
|
+
Object.getPrototypeOf(runtime)
|
|
27
|
+
)) {
|
|
28
|
+
if (key === "constructor") {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const value = (runtime as unknown as AnyRecord)[key];
|
|
33
|
+
|
|
34
|
+
if (typeof value === "function") {
|
|
35
|
+
target[key] = (...args: unknown[]) => {
|
|
36
|
+
const result = (value as (...args: unknown[]) => unknown).apply(
|
|
37
|
+
runtime,
|
|
38
|
+
args
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Lifecycle methods (where, extend, db) return a new ModelRuntime.
|
|
42
|
+
// Re-wrap it so custom methods are re-attached on the fresh target.
|
|
43
|
+
if (result instanceof ModelRuntime) {
|
|
44
|
+
return buildTarget(result);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Object.defineProperty(target, "$model", {
|
|
53
|
+
get: () => runtime.$model,
|
|
54
|
+
enumerable: true,
|
|
55
|
+
});
|
|
56
|
+
Object.defineProperty(target, "$modelName", {
|
|
57
|
+
get: () => runtime.$modelName,
|
|
58
|
+
enumerable: true,
|
|
59
|
+
});
|
|
60
|
+
Object.defineProperty(target, "$format", {
|
|
61
|
+
get: () => runtime.$format,
|
|
62
|
+
enumerable: true,
|
|
63
|
+
});
|
|
64
|
+
Object.defineProperty(target, "$where", {
|
|
65
|
+
get: () => runtime.$where,
|
|
66
|
+
enumerable: true,
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(target, "$tableName", {
|
|
69
|
+
get: () => runtime.$tableName,
|
|
70
|
+
enumerable: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Attach user-defined methods, binding each to this target so that `this`
|
|
74
|
+
// inside a custom method refers to the model-like object (with all methods).
|
|
75
|
+
runtime.attachMethods(target);
|
|
76
|
+
|
|
77
|
+
return target;
|
|
78
|
+
}
|
|
79
|
+
|
|
8
80
|
export function modelBuilder<
|
|
9
81
|
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
10
82
|
TRelations extends AnyRelations = EmptyRelations,
|
|
@@ -44,36 +116,7 @@ export function modelBuilder<
|
|
|
44
116
|
options: (options ?? {}) as any,
|
|
45
117
|
});
|
|
46
118
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
for (const key of Object.getOwnPropertyNames(
|
|
50
|
-
Object.getPrototypeOf(runtime)
|
|
51
|
-
)) {
|
|
52
|
-
if (key === "constructor") {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
const value = (runtime as unknown as Record<string, unknown>)[key];
|
|
56
|
-
if (typeof value === "function") {
|
|
57
|
-
target[key] = value.bind(runtime);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
Object.defineProperty(target, "$model", {
|
|
62
|
-
get: () => runtime.$model,
|
|
63
|
-
enumerable: true,
|
|
64
|
-
});
|
|
65
|
-
Object.defineProperty(target, "$modelName", {
|
|
66
|
-
get: () => runtime.$modelName,
|
|
67
|
-
enumerable: true,
|
|
68
|
-
});
|
|
69
|
-
Object.defineProperty(target, "$format", {
|
|
70
|
-
get: () => runtime.$format,
|
|
71
|
-
enumerable: true,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
runtime.attachMethods(target);
|
|
75
|
-
|
|
76
|
-
return target as unknown as Model<
|
|
119
|
+
return buildTarget(runtime) as unknown as Model<
|
|
77
120
|
ModelConfig<TRelations, TRelations[TTableName], TDialect, TOptions>
|
|
78
121
|
>;
|
|
79
122
|
};
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { model } from "tests/base";
|
|
3
|
+
import { db } from "tests/db";
|
|
4
|
+
import { esc } from "@/model";
|
|
5
|
+
|
|
6
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function uid(): string {
|
|
9
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ─── Models ───────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A user model that defines several custom methods.
|
|
16
|
+
* These must remain accessible after every lifecycle call
|
|
17
|
+
* (.where, .extend, .db).
|
|
18
|
+
*/
|
|
19
|
+
const userModel = model("user", {});
|
|
20
|
+
|
|
21
|
+
const serviceQueries = userModel.extend({
|
|
22
|
+
methods: {
|
|
23
|
+
findByName(name: string) {
|
|
24
|
+
return serviceQueries.where({ name: esc(name) }).findFirst();
|
|
25
|
+
},
|
|
26
|
+
findAllVerified() {
|
|
27
|
+
return serviceQueries.where({ isVerified: esc(true) }).findMany();
|
|
28
|
+
},
|
|
29
|
+
findByMinAge(minAge: number) {
|
|
30
|
+
return serviceQueries.where({ age: { gte: esc(minAge) } }).findMany();
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const postsModel = model("userPosts", {});
|
|
36
|
+
|
|
37
|
+
const postQueries = postsModel.extend({
|
|
38
|
+
methods: {
|
|
39
|
+
findByTitle(title: string) {
|
|
40
|
+
return postQueries.where({ title: esc(title) }).findFirst();
|
|
41
|
+
},
|
|
42
|
+
findFeatured() {
|
|
43
|
+
return postQueries.where({ featured: esc(true) }).findMany();
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
describe("custom methods", () => {
|
|
51
|
+
// -------------------------------------------------------------------------
|
|
52
|
+
// Presence — methods must be callable on the initial model object
|
|
53
|
+
// -------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
describe("presence on initial model", () => {
|
|
56
|
+
test("all declared methods are functions on the model", () => {
|
|
57
|
+
expect(typeof serviceQueries.findByName).toBe("function");
|
|
58
|
+
expect(typeof serviceQueries.findAllVerified).toBe("function");
|
|
59
|
+
expect(typeof serviceQueries.findByMinAge).toBe("function");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("multiple models keep their own method sets independent", () => {
|
|
63
|
+
expect(typeof serviceQueries.findByName).toBe("function");
|
|
64
|
+
expect(typeof postQueries.findByTitle).toBe("function");
|
|
65
|
+
// cross-model — methods must NOT bleed across models
|
|
66
|
+
expect(
|
|
67
|
+
(serviceQueries as { findByTitle?: unknown }).findByTitle
|
|
68
|
+
).toBeUndefined();
|
|
69
|
+
expect(
|
|
70
|
+
(postQueries as { findByName?: unknown }).findByName
|
|
71
|
+
).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
// Survival through .where() — THE PRIMARY BUG
|
|
77
|
+
// Custom methods must survive every `.where()` call.
|
|
78
|
+
// -------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe("survival after .where()", () => {
|
|
81
|
+
test("custom methods are present on the result of .where()", () => {
|
|
82
|
+
const scoped = serviceQueries.where({ age: { gte: esc(0) } });
|
|
83
|
+
expect(typeof scoped.findByName).toBe("function");
|
|
84
|
+
expect(typeof scoped.findAllVerified).toBe("function");
|
|
85
|
+
expect(typeof scoped.findByMinAge).toBe("function");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("custom methods are present after chained .where() calls", () => {
|
|
89
|
+
const scoped = serviceQueries
|
|
90
|
+
.where({ age: { gte: esc(0) } })
|
|
91
|
+
.where({ isVerified: esc(false) });
|
|
92
|
+
expect(typeof scoped.findByName).toBe("function");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("base methods are still present after .where()", () => {
|
|
96
|
+
const scoped = serviceQueries.where({ age: { gte: esc(0) } });
|
|
97
|
+
expect(typeof scoped.findFirst).toBe("function");
|
|
98
|
+
expect(typeof scoped.findMany).toBe("function");
|
|
99
|
+
expect(typeof scoped.insert).toBe("function");
|
|
100
|
+
expect(typeof scoped.update).toBe("function");
|
|
101
|
+
expect(typeof scoped.delete).toBe("function");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// Survival through .extend()
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
describe("survival after .extend()", () => {
|
|
110
|
+
test("original custom methods survive .extend()", () => {
|
|
111
|
+
const extended = serviceQueries.extend({});
|
|
112
|
+
expect(typeof extended.findByName).toBe("function");
|
|
113
|
+
expect(typeof extended.findAllVerified).toBe("function");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("new methods added via .extend() are accessible", () => {
|
|
117
|
+
const extended = serviceQueries.extend({
|
|
118
|
+
methods: {
|
|
119
|
+
findAdults() {
|
|
120
|
+
return extended.where({ age: { gte: esc(18) } }).findMany();
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
expect(typeof extended.findAdults).toBe("function");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("parent custom methods survive on an extended model", () => {
|
|
128
|
+
const extended = serviceQueries.extend({
|
|
129
|
+
methods: {
|
|
130
|
+
findAdults() {
|
|
131
|
+
return extended.where({ age: { gte: esc(18) } }).findMany();
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
// Parent methods must still be present on the child
|
|
136
|
+
expect(typeof extended.findByName).toBe("function");
|
|
137
|
+
expect(typeof extended.findAllVerified).toBe("function");
|
|
138
|
+
// New method must be present too
|
|
139
|
+
expect(typeof extended.findAdults).toBe("function");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("child method overrides parent method of the same name", () => {
|
|
143
|
+
const SENTINEL = Symbol("override-sentinel");
|
|
144
|
+
const extended = serviceQueries.extend({
|
|
145
|
+
methods: {
|
|
146
|
+
findByName(_name: string) {
|
|
147
|
+
return SENTINEL;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
// The extended model's findByName must shadow the parent's
|
|
152
|
+
expect(extended.findByName("ignored")).toBe(SENTINEL);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("custom methods survive .where() on an extended model", () => {
|
|
156
|
+
const extended = serviceQueries.extend({
|
|
157
|
+
methods: {
|
|
158
|
+
findAdults() {
|
|
159
|
+
return extended.where({ age: { gte: esc(18) } }).findMany();
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
const scoped = extended.where({ isVerified: esc(true) });
|
|
164
|
+
expect(typeof scoped.findByName).toBe("function");
|
|
165
|
+
expect(typeof scoped.findAdults).toBe("function");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// -------------------------------------------------------------------------
|
|
170
|
+
// Survival through .db()
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
describe("survival after .db()", () => {
|
|
174
|
+
test("custom methods are present after .db() rebind", () => {
|
|
175
|
+
const rebound = serviceQueries.db(db);
|
|
176
|
+
expect(typeof rebound.findByName).toBe("function");
|
|
177
|
+
expect(typeof rebound.findAllVerified).toBe("function");
|
|
178
|
+
expect(typeof rebound.findByMinAge).toBe("function");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("base methods are still present after .db() rebind", () => {
|
|
182
|
+
const rebound = serviceQueries.db(db);
|
|
183
|
+
expect(typeof rebound.findFirst).toBe("function");
|
|
184
|
+
expect(typeof rebound.findMany).toBe("function");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("custom methods survive .where() after .db() rebind", () => {
|
|
188
|
+
const rebound = serviceQueries.db(db).where({ age: { gte: esc(0) } });
|
|
189
|
+
expect(typeof rebound.findByName).toBe("function");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
194
|
+
// Functional correctness — custom methods actually return the right data
|
|
195
|
+
// -------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
describe("functional correctness", () => {
|
|
198
|
+
test("findByName returns a single user with matching name", async () => {
|
|
199
|
+
const uniqueName = `TestUser-${uid()}`;
|
|
200
|
+
const uniqueEmail = `${uid()}@custom-methods.test`;
|
|
201
|
+
|
|
202
|
+
await serviceQueries.insert({
|
|
203
|
+
name: uniqueName,
|
|
204
|
+
email: uniqueEmail,
|
|
205
|
+
age: 25,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const result = await serviceQueries.findByName(uniqueName);
|
|
209
|
+
|
|
210
|
+
expect(result).toBeDefined();
|
|
211
|
+
expect(result?.name).toBe(uniqueName);
|
|
212
|
+
expect(typeof result?.id).toBe("number");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("findAllVerified returns only verified users", async () => {
|
|
216
|
+
const result = await serviceQueries.findAllVerified();
|
|
217
|
+
|
|
218
|
+
expect(Array.isArray(result)).toBe(true);
|
|
219
|
+
for (const user of result) {
|
|
220
|
+
expect(user.isVerified).toBe(true);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("findByMinAge returns users at or above the age threshold", async () => {
|
|
225
|
+
const result = await serviceQueries.findByMinAge(18);
|
|
226
|
+
|
|
227
|
+
expect(Array.isArray(result)).toBe(true);
|
|
228
|
+
for (const user of result) {
|
|
229
|
+
expect(user.age).toBeGreaterThanOrEqual(18);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("custom method result is consistent with equivalent base query", async () => {
|
|
234
|
+
const uniqueName = `Consistent-${uid()}`;
|
|
235
|
+
const uniqueEmail = `${uid()}@custom-methods.test`;
|
|
236
|
+
|
|
237
|
+
await serviceQueries.insert({
|
|
238
|
+
name: uniqueName,
|
|
239
|
+
email: uniqueEmail,
|
|
240
|
+
age: 30,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const viaCustom = await serviceQueries.findByName(uniqueName);
|
|
244
|
+
const viaBase = await serviceQueries
|
|
245
|
+
.where({ name: esc(uniqueName) })
|
|
246
|
+
.findFirst();
|
|
247
|
+
|
|
248
|
+
expect(viaCustom).toEqual(viaBase);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("posts custom method findByTitle returns correct post", async () => {
|
|
252
|
+
const uniqueTitle = `PostTitle-${uid()}`;
|
|
253
|
+
|
|
254
|
+
await postQueries.insert({
|
|
255
|
+
title: uniqueTitle,
|
|
256
|
+
featured: false,
|
|
257
|
+
userId: 1,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const result = await postQueries.findByTitle(uniqueTitle);
|
|
261
|
+
|
|
262
|
+
expect(result).toBeDefined();
|
|
263
|
+
expect(result?.title).toBe(uniqueTitle);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("posts custom method findFeatured returns only featured posts", async () => {
|
|
267
|
+
await postQueries.insert({
|
|
268
|
+
title: `Featured-${uid()}`,
|
|
269
|
+
featured: true,
|
|
270
|
+
userId: 1,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const result = await postQueries.findFeatured();
|
|
274
|
+
|
|
275
|
+
expect(Array.isArray(result)).toBe(true);
|
|
276
|
+
expect(result.length).toBeGreaterThan(0);
|
|
277
|
+
for (const post of result) {
|
|
278
|
+
expect(post.featured).toBe(true);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("custom method is still functional after .where() (the bug scenario)", async () => {
|
|
283
|
+
const uniqueName = `BugCheck-${uid()}`;
|
|
284
|
+
const uniqueEmail = `${uid()}@custom-methods.test`;
|
|
285
|
+
|
|
286
|
+
await serviceQueries.insert({
|
|
287
|
+
name: uniqueName,
|
|
288
|
+
email: uniqueEmail,
|
|
289
|
+
age: 99,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const scopedModel = serviceQueries.where({ age: { gte: esc(0) } });
|
|
293
|
+
const result = await scopedModel.findByName(uniqueName);
|
|
294
|
+
|
|
295
|
+
expect(result).toBeDefined();
|
|
296
|
+
expect(result?.name).toBe(uniqueName);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("custom method is still functional after .extend()", async () => {
|
|
300
|
+
const uniqueName = `ExtendBug-${uid()}`;
|
|
301
|
+
const uniqueEmail = `${uid()}@custom-methods.test`;
|
|
302
|
+
|
|
303
|
+
await serviceQueries.insert({
|
|
304
|
+
name: uniqueName,
|
|
305
|
+
email: uniqueEmail,
|
|
306
|
+
age: 22,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const extended = serviceQueries.extend({
|
|
310
|
+
format: (u) => ({ ...u, _extended: true }),
|
|
311
|
+
});
|
|
312
|
+
const result = await extended.findByName(uniqueName);
|
|
313
|
+
|
|
314
|
+
expect(result).toBeDefined();
|
|
315
|
+
expect(result?.name).toBe(uniqueName);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("custom method is still functional after .db()", async () => {
|
|
319
|
+
const uniqueName = `DbBug-${uid()}`;
|
|
320
|
+
const uniqueEmail = `${uid()}@custom-methods.test`;
|
|
321
|
+
|
|
322
|
+
await serviceQueries.insert({
|
|
323
|
+
name: uniqueName,
|
|
324
|
+
email: uniqueEmail,
|
|
325
|
+
age: 33,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const rebound = serviceQueries.db(db);
|
|
329
|
+
const result = await rebound.findByName(uniqueName);
|
|
330
|
+
|
|
331
|
+
expect(result).toBeDefined();
|
|
332
|
+
expect(result?.name).toBe(uniqueName);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|