@bunyad/orm 0.1.0-alpha.0
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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/casts.d.ts +50 -0
- package/dist/casts.js +309 -0
- package/dist/concerns.d.ts +64 -0
- package/dist/concerns.js +216 -0
- package/dist/decorators.d.ts +7 -0
- package/dist/decorators.js +12 -0
- package/dist/eager-fetch.d.ts +7 -0
- package/dist/eager-fetch.js +38 -0
- package/dist/eager.d.ts +24 -0
- package/dist/eager.js +854 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +265 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/model-events.d.ts +43 -0
- package/dist/model-events.js +138 -0
- package/dist/model-helpers.d.ts +32 -0
- package/dist/model-helpers.js +99 -0
- package/dist/model-query.d.ts +312 -0
- package/dist/model-query.js +3170 -0
- package/dist/model-strictness.d.ts +35 -0
- package/dist/model-strictness.js +114 -0
- package/dist/model.d.ts +490 -0
- package/dist/model.js +1931 -0
- package/dist/orm-collection.d.ts +37 -0
- package/dist/orm-collection.js +150 -0
- package/dist/pivot.d.ts +9 -0
- package/dist/pivot.js +9 -0
- package/dist/relations.d.ts +335 -0
- package/dist/relations.js +1164 -0
- package/dist/scopes.d.ts +36 -0
- package/dist/scopes.js +128 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shah Khalid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @bunyad/orm
|
|
2
|
+
|
|
3
|
+
Active-record models on top of [`@bunyad/database`](https://www.npmjs.com/package/@bunyad/database): relations, casts, scopes, events, soft deletes and factories. The same model code runs on Node and Bun.
|
|
4
|
+
|
|
5
|
+
> **Alpha.** APIs may change between `0.x` releases.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @bunyad/orm@alpha @bunyad/database@alpha better-sqlite3
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { connectSqlite, schemaFor } from "@bunyad/database";
|
|
15
|
+
import { Model } from "@bunyad/orm";
|
|
16
|
+
|
|
17
|
+
const connection = connectSqlite({ path: "app.sqlite" });
|
|
18
|
+
Model.setConnection(connection);
|
|
19
|
+
|
|
20
|
+
await schemaFor(connection).create("users", (table) => {
|
|
21
|
+
table.id();
|
|
22
|
+
table.string("name");
|
|
23
|
+
table.timestamps();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
class User extends Model {
|
|
27
|
+
static table = "users";
|
|
28
|
+
static fillable = ["name"] as const;
|
|
29
|
+
|
|
30
|
+
declare id: number;
|
|
31
|
+
declare name: string;
|
|
32
|
+
|
|
33
|
+
posts() {
|
|
34
|
+
return this.hasMany(Post);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class Post extends Model {
|
|
39
|
+
static table = "posts";
|
|
40
|
+
|
|
41
|
+
user() {
|
|
42
|
+
return this.belongsTo(User);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ada = await User.create({ name: "Ada" });
|
|
47
|
+
await User.find(ada.id);
|
|
48
|
+
await User.where("name", "Ada").first();
|
|
49
|
+
await User.query().count();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Relations load lazily (`await user.posts().get()`) or eagerly (`User.with("posts")`).
|
|
53
|
+
|
|
54
|
+
## Drivers
|
|
55
|
+
|
|
56
|
+
Pick one and install it next to `@bunyad/database`: `better-sqlite3`, `pg` or `mysql2`. Bun uses its built-in drivers.
|
|
57
|
+
|
|
58
|
+
## Framework glue
|
|
59
|
+
|
|
60
|
+
- Express on Node: [`docs/examples/orm-node-express`](https://github.com/bunyadjs/framework/tree/main/docs/examples/orm-node-express)
|
|
61
|
+
- Next.js (server-only, one shared pool): [`docs/examples/orm-node-next`](https://github.com/bunyadjs/framework/tree/main/docs/examples/orm-node-next)
|
|
62
|
+
- NestJS: `@bunyad/nestjs` (`BunyadOrmModule`), in the monorepo and not published yet.
|
|
63
|
+
|
|
64
|
+
## Status
|
|
65
|
+
|
|
66
|
+
Alpha. SQLite is the most tested driver; PostgreSQL and MySQL tests need a live database and are skipped by default. Aggregate helpers such as `sumCase`/`countCase` are not implemented yet.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/dist/casts.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribute cast accessor/mutator (`Attribute.make`).
|
|
3
|
+
*/
|
|
4
|
+
export declare class Attribute {
|
|
5
|
+
readonly get?: (value: unknown, attributes: Record<string, unknown>) => unknown;
|
|
6
|
+
readonly set?: (value: unknown, attributes: Record<string, unknown>) => unknown | Record<string, unknown>;
|
|
7
|
+
constructor(options: {
|
|
8
|
+
get?: (value: unknown, attributes: Record<string, unknown>) => unknown;
|
|
9
|
+
set?: (value: unknown, attributes: Record<string, unknown>) => unknown | Record<string, unknown>;
|
|
10
|
+
});
|
|
11
|
+
/** `Attribute.make({ get, set })`. */
|
|
12
|
+
static make(options: {
|
|
13
|
+
get?: (value: unknown, attributes: Record<string, unknown>) => unknown;
|
|
14
|
+
set?: (value: unknown, attributes: Record<string, unknown>) => unknown | Record<string, unknown>;
|
|
15
|
+
}): Attribute;
|
|
16
|
+
}
|
|
17
|
+
/** Built-in cast type names. */
|
|
18
|
+
export type CastType = "boolean" | "bool" | "number" | "integer" | "int" | "float" | "double" | "real" | "decimal" | "bigint" | "string" | "json" | "date" | "datetime" | "array" | "collection" | "encrypted" | "encrypted:array" | "encrypted:json" | "encrypted:collection" | "hashed";
|
|
19
|
+
/**
|
|
20
|
+
* A TypeScript string/number enum object (or similar value map).
|
|
21
|
+
*/
|
|
22
|
+
export type EnumCast = Record<string, string | number>;
|
|
23
|
+
/** Anything accepted in `casts` / `casts()`. */
|
|
24
|
+
export type CastDefinition = CastType | EnumCast | Attribute;
|
|
25
|
+
export declare function isCastType(value: unknown): value is CastType;
|
|
26
|
+
export declare function isAttribute(value: unknown): value is Attribute;
|
|
27
|
+
export declare function isEnumCast(value: unknown): value is EnumCast;
|
|
28
|
+
/** Async bcrypt for the `hashed` cast (same output as the sync path, off the event loop). */
|
|
29
|
+
export declare function hashCastValue(value: string): Promise<string>;
|
|
30
|
+
export declare function isAlreadyHashed(value: string): boolean;
|
|
31
|
+
export declare function castFromStorage(value: unknown, type: CastType): unknown;
|
|
32
|
+
export declare function castToStorage(value: unknown, type: CastType, driver?: string): unknown;
|
|
33
|
+
export declare function enumFromStorage(value: unknown, enumObj: EnumCast): unknown;
|
|
34
|
+
export declare function enumToStorage(value: unknown, enumObj: EnumCast): unknown;
|
|
35
|
+
/** Cast JSON column to a `Collection`. */
|
|
36
|
+
export declare const AsCollection: Attribute;
|
|
37
|
+
/** Cast JSON column to a `Fluent` bag. */
|
|
38
|
+
export declare const AsFluent: Attribute;
|
|
39
|
+
/**
|
|
40
|
+
* Cast a JSON array to a Collection of enum values.
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* static casts() {
|
|
44
|
+
* return { statuses: AsEnumCollection.of(Status) };
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare const AsEnumCollection: {
|
|
49
|
+
of(enumObj: EnumCast): Attribute;
|
|
50
|
+
};
|
package/dist/casts.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { collect, Crypt, Fluent } from "@bunyad/common";
|
|
2
|
+
import { dateForStorage, dateTimeForStorage, toDate, password as passwordPlatform, } from "@bunyad/database";
|
|
3
|
+
/**
|
|
4
|
+
* Attribute cast accessor/mutator (`Attribute.make`).
|
|
5
|
+
*/
|
|
6
|
+
export class Attribute {
|
|
7
|
+
get;
|
|
8
|
+
set;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.get = options.get;
|
|
11
|
+
this.set = options.set;
|
|
12
|
+
}
|
|
13
|
+
/** `Attribute.make({ get, set })`. */
|
|
14
|
+
static make(options) {
|
|
15
|
+
return new Attribute(options);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const CAST_TYPE_SET = new Set([
|
|
19
|
+
"boolean",
|
|
20
|
+
"bool",
|
|
21
|
+
"number",
|
|
22
|
+
"integer",
|
|
23
|
+
"int",
|
|
24
|
+
"float",
|
|
25
|
+
"double",
|
|
26
|
+
"real",
|
|
27
|
+
"decimal",
|
|
28
|
+
"bigint",
|
|
29
|
+
"string",
|
|
30
|
+
"json",
|
|
31
|
+
"date",
|
|
32
|
+
"datetime",
|
|
33
|
+
"array",
|
|
34
|
+
"collection",
|
|
35
|
+
"encrypted",
|
|
36
|
+
"encrypted:array",
|
|
37
|
+
"encrypted:json",
|
|
38
|
+
"encrypted:collection",
|
|
39
|
+
"hashed",
|
|
40
|
+
]);
|
|
41
|
+
function toNumber(value) {
|
|
42
|
+
if (typeof value === "number") {
|
|
43
|
+
return Number.isFinite(value) ? value : 0;
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "bigint") {
|
|
46
|
+
return Number(value);
|
|
47
|
+
}
|
|
48
|
+
if (value === "" || value === null || value === undefined) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const n = Number(value);
|
|
52
|
+
return Number.isFinite(n) ? n : 0;
|
|
53
|
+
}
|
|
54
|
+
/** JSON/sync often send `"0"` / `"false"`; those must not become true. */
|
|
55
|
+
function toBoolean(value) {
|
|
56
|
+
if (value === true || value === 1 || value === 1n || value === "1") {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (value === false || value === 0 || value === 0n || value === "0") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "string") {
|
|
63
|
+
const normalized = value.trim().toLowerCase();
|
|
64
|
+
if (normalized === "false" ||
|
|
65
|
+
normalized === "no" ||
|
|
66
|
+
normalized === "off" ||
|
|
67
|
+
normalized === "") {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (normalized === "true" ||
|
|
71
|
+
normalized === "yes" ||
|
|
72
|
+
normalized === "on") {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Boolean(value);
|
|
77
|
+
}
|
|
78
|
+
export function isCastType(value) {
|
|
79
|
+
return typeof value === "string" && CAST_TYPE_SET.has(value);
|
|
80
|
+
}
|
|
81
|
+
export function isAttribute(value) {
|
|
82
|
+
return value instanceof Attribute;
|
|
83
|
+
}
|
|
84
|
+
export function isEnumCast(value) {
|
|
85
|
+
if (value == null || typeof value !== "object")
|
|
86
|
+
return false;
|
|
87
|
+
if (isAttribute(value) || Array.isArray(value))
|
|
88
|
+
return false;
|
|
89
|
+
const entries = Object.entries(value);
|
|
90
|
+
if (entries.length === 0)
|
|
91
|
+
return false;
|
|
92
|
+
return entries.every(([, v]) => typeof v === "string" || typeof v === "number");
|
|
93
|
+
}
|
|
94
|
+
function parseDateValue(value) {
|
|
95
|
+
return toDate(value);
|
|
96
|
+
}
|
|
97
|
+
/** Async bcrypt for the `hashed` cast (same output as the sync path, off the event loop). */
|
|
98
|
+
export function hashCastValue(value) {
|
|
99
|
+
return passwordPlatform.hash(value, {
|
|
100
|
+
algorithm: "bcrypt",
|
|
101
|
+
cost: bcryptRounds(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Same cost as `Hash.make`: `BCRYPT_ROUNDS`, default 10. */
|
|
105
|
+
function bcryptRounds() {
|
|
106
|
+
return Number(process.env.BCRYPT_ROUNDS ?? 10);
|
|
107
|
+
}
|
|
108
|
+
export function isAlreadyHashed(value) {
|
|
109
|
+
return value.startsWith("$2") || value.startsWith("$argon2");
|
|
110
|
+
}
|
|
111
|
+
function collectionItems(value) {
|
|
112
|
+
if (value &&
|
|
113
|
+
typeof value === "object" &&
|
|
114
|
+
typeof value.all === "function") {
|
|
115
|
+
return value.all();
|
|
116
|
+
}
|
|
117
|
+
return Array.isArray(value) ? value : [];
|
|
118
|
+
}
|
|
119
|
+
function encryptPayload(plain) {
|
|
120
|
+
const text = typeof plain === "string" ? plain : JSON.stringify(plain);
|
|
121
|
+
return Crypt.encrypt(text);
|
|
122
|
+
}
|
|
123
|
+
function decryptPayload(value) {
|
|
124
|
+
if (typeof value !== "string")
|
|
125
|
+
return String(value ?? "");
|
|
126
|
+
return Crypt.decrypt(value);
|
|
127
|
+
}
|
|
128
|
+
export function castFromStorage(value, type) {
|
|
129
|
+
if (value == null)
|
|
130
|
+
return value;
|
|
131
|
+
switch (type) {
|
|
132
|
+
case "boolean":
|
|
133
|
+
case "bool":
|
|
134
|
+
return toBoolean(value);
|
|
135
|
+
case "number":
|
|
136
|
+
case "float":
|
|
137
|
+
case "double":
|
|
138
|
+
case "real":
|
|
139
|
+
case "decimal":
|
|
140
|
+
return toNumber(value);
|
|
141
|
+
case "integer":
|
|
142
|
+
case "int":
|
|
143
|
+
return Math.trunc(toNumber(value));
|
|
144
|
+
case "bigint":
|
|
145
|
+
return typeof value === "bigint" ? value : BigInt(String(value));
|
|
146
|
+
case "string":
|
|
147
|
+
return String(value);
|
|
148
|
+
case "json":
|
|
149
|
+
case "array":
|
|
150
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
151
|
+
case "collection": {
|
|
152
|
+
// From the database (JSON text), or already in memory: `fill()` passes a Collection or array.
|
|
153
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
154
|
+
return collect(collectionItems(parsed));
|
|
155
|
+
}
|
|
156
|
+
case "date":
|
|
157
|
+
case "datetime":
|
|
158
|
+
return parseDateValue(value);
|
|
159
|
+
case "encrypted":
|
|
160
|
+
return decryptPayload(value);
|
|
161
|
+
case "encrypted:json":
|
|
162
|
+
case "encrypted:array": {
|
|
163
|
+
const plain = decryptPayload(value);
|
|
164
|
+
return JSON.parse(plain);
|
|
165
|
+
}
|
|
166
|
+
case "encrypted:collection": {
|
|
167
|
+
const plain = decryptPayload(value);
|
|
168
|
+
const parsed = JSON.parse(plain);
|
|
169
|
+
return collect(Array.isArray(parsed) ? parsed : []);
|
|
170
|
+
}
|
|
171
|
+
case "hashed":
|
|
172
|
+
return value;
|
|
173
|
+
default:
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export function castToStorage(value, type, driver) {
|
|
178
|
+
if (value == null)
|
|
179
|
+
return value;
|
|
180
|
+
const driverName = (driver ?? "sqlite");
|
|
181
|
+
switch (type) {
|
|
182
|
+
case "boolean":
|
|
183
|
+
case "bool": {
|
|
184
|
+
const flag = toBoolean(value);
|
|
185
|
+
if (driverName === "postgres")
|
|
186
|
+
return flag;
|
|
187
|
+
return flag ? 1 : 0;
|
|
188
|
+
}
|
|
189
|
+
case "number":
|
|
190
|
+
case "float":
|
|
191
|
+
case "double":
|
|
192
|
+
case "real":
|
|
193
|
+
case "decimal":
|
|
194
|
+
return toNumber(value);
|
|
195
|
+
case "integer":
|
|
196
|
+
case "int":
|
|
197
|
+
return Math.trunc(toNumber(value));
|
|
198
|
+
case "bigint":
|
|
199
|
+
return typeof value === "bigint" ? value : BigInt(String(value));
|
|
200
|
+
case "string":
|
|
201
|
+
return String(value);
|
|
202
|
+
case "json":
|
|
203
|
+
case "array":
|
|
204
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
205
|
+
case "collection": {
|
|
206
|
+
const items = collectionItems(value);
|
|
207
|
+
return JSON.stringify(items);
|
|
208
|
+
}
|
|
209
|
+
case "date":
|
|
210
|
+
return dateForStorage(value);
|
|
211
|
+
case "datetime":
|
|
212
|
+
return dateTimeForStorage(value, driverName);
|
|
213
|
+
case "encrypted":
|
|
214
|
+
return encryptPayload(value);
|
|
215
|
+
case "encrypted:json":
|
|
216
|
+
case "encrypted:array":
|
|
217
|
+
return encryptPayload(typeof value === "string" ? JSON.parse(value) : value);
|
|
218
|
+
case "encrypted:collection":
|
|
219
|
+
return encryptPayload(collectionItems(value));
|
|
220
|
+
case "hashed": {
|
|
221
|
+
if (typeof value !== "string" || value === "")
|
|
222
|
+
return value;
|
|
223
|
+
// Match prior ORM behaviour: never double-hash `$2…` / `$argon2…`.
|
|
224
|
+
if (isAlreadyHashed(value))
|
|
225
|
+
return value;
|
|
226
|
+
return passwordPlatform.hashSync(value, {
|
|
227
|
+
algorithm: "bcrypt",
|
|
228
|
+
cost: bcryptRounds(),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
default:
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export function enumFromStorage(value, enumObj) {
|
|
236
|
+
if (value == null)
|
|
237
|
+
return value;
|
|
238
|
+
for (const entry of Object.values(enumObj)) {
|
|
239
|
+
if (entry == value || String(entry) === String(value))
|
|
240
|
+
return entry;
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
export function enumToStorage(value, enumObj) {
|
|
245
|
+
if (value == null)
|
|
246
|
+
return value;
|
|
247
|
+
for (const entry of Object.values(enumObj)) {
|
|
248
|
+
if (entry === value)
|
|
249
|
+
return entry;
|
|
250
|
+
}
|
|
251
|
+
if (typeof value === "string" && value in enumObj) {
|
|
252
|
+
return enumObj[value];
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
/** Cast JSON column to a `Collection`. */
|
|
257
|
+
export const AsCollection = Attribute.make({
|
|
258
|
+
get(value) {
|
|
259
|
+
return castFromStorage(value, "collection");
|
|
260
|
+
},
|
|
261
|
+
set(value) {
|
|
262
|
+
return castToStorage(value, "collection");
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
/** Cast JSON column to a `Fluent` bag. */
|
|
266
|
+
export const AsFluent = Attribute.make({
|
|
267
|
+
get(value) {
|
|
268
|
+
if (value == null)
|
|
269
|
+
return new Fluent();
|
|
270
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
271
|
+
return new Fluent(parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
272
|
+
? parsed
|
|
273
|
+
: {});
|
|
274
|
+
},
|
|
275
|
+
set(value) {
|
|
276
|
+
if (value instanceof Fluent)
|
|
277
|
+
return JSON.stringify(value.all());
|
|
278
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
279
|
+
return JSON.stringify(value);
|
|
280
|
+
}
|
|
281
|
+
return JSON.stringify({});
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
/**
|
|
285
|
+
* Cast a JSON array to a Collection of enum values.
|
|
286
|
+
*
|
|
287
|
+
* ```ts
|
|
288
|
+
* static casts() {
|
|
289
|
+
* return { statuses: AsEnumCollection.of(Status) };
|
|
290
|
+
* }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export const AsEnumCollection = {
|
|
294
|
+
of(enumObj) {
|
|
295
|
+
return Attribute.make({
|
|
296
|
+
get(value) {
|
|
297
|
+
if (value == null)
|
|
298
|
+
return collect([]);
|
|
299
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
300
|
+
const list = Array.isArray(parsed) ? parsed : [];
|
|
301
|
+
return collect(list.map((item) => enumFromStorage(item, enumObj)));
|
|
302
|
+
},
|
|
303
|
+
set(value) {
|
|
304
|
+
const items = collectionItems(value).map((item) => enumToStorage(item, enumObj));
|
|
305
|
+
return JSON.stringify(items);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ModelClass } from "./model.js";
|
|
2
|
+
import type { Factory } from "./factory.js";
|
|
3
|
+
/** Generate a UUIDv7 (time-ordered) string. */
|
|
4
|
+
export declare function uuid7(): string;
|
|
5
|
+
/** Generate a 26-character ULID. */
|
|
6
|
+
export declare function ulid(now?: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* Ordered UUID primary keys (`incrementing = false`).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* @HasUuids()
|
|
13
|
+
* class Article extends Model {}
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function HasUuids(): ClassDecorator;
|
|
17
|
+
/**
|
|
18
|
+
* ULID primary keys (`incrementing = false`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function HasUlids(): ClassDecorator;
|
|
21
|
+
type FactoryClass = new () => Factory;
|
|
22
|
+
type FactoryResolver = FactoryClass | (() => Factory);
|
|
23
|
+
/**
|
|
24
|
+
* Bind a factory class and expose `Model.factory()`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* @HasFactory(UserFactory)
|
|
29
|
+
* // or @HasFactory(() => new UserFactory())
|
|
30
|
+
* class User extends Model {}
|
|
31
|
+
* User.factory().create();
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function HasFactory(factory: FactoryResolver): ClassDecorator;
|
|
35
|
+
/**
|
|
36
|
+
* Soft deletes — sets `static softDeletes = true`.
|
|
37
|
+
* Prefer this decorator for `use SoftDeletes`-style call sites.
|
|
38
|
+
*/
|
|
39
|
+
export declare function SoftDeletes(column?: string): ClassDecorator;
|
|
40
|
+
/**
|
|
41
|
+
* Register for `model:prune` / `pruneAll()`.
|
|
42
|
+
* Define `static prunable()` returning the query of models to delete.
|
|
43
|
+
*/
|
|
44
|
+
export declare function Prunable(): ClassDecorator;
|
|
45
|
+
/**
|
|
46
|
+
* Mass-delete via query (no model hydration / events).
|
|
47
|
+
*/
|
|
48
|
+
export declare function MassPrunable(): ClassDecorator;
|
|
49
|
+
export declare function prunableModelClasses(): ModelClass[];
|
|
50
|
+
export declare function massPrunableModelClasses(): ModelClass[];
|
|
51
|
+
/** Clear registries (tests). */
|
|
52
|
+
export declare function clearPrunableRegistries(): void;
|
|
53
|
+
/** Prune one model class (`Model::pruneAll` for a single class). */
|
|
54
|
+
export declare function prune(ModelClass: ModelClass, options?: {
|
|
55
|
+
chunk?: number;
|
|
56
|
+
}): Promise<number>;
|
|
57
|
+
/** Prune every registered `@Prunable` / `@MassPrunable` model. */
|
|
58
|
+
export declare function pruneAll(options?: {
|
|
59
|
+
chunk?: number;
|
|
60
|
+
}): Promise<Array<{
|
|
61
|
+
model: string;
|
|
62
|
+
count: number;
|
|
63
|
+
}>>;
|
|
64
|
+
export {};
|