@zerotal/orm 1.1.0 → 1.4.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/CHANGELOG.md +96 -1
- package/README.md +3 -3
- package/package.json +3 -3
- package/src/casts/encrypted.ts +168 -0
- package/src/index.ts +18 -11
- package/src/model/BaseModel.ts +102 -9
- package/src/model/ModelQueryBuilder.ts +18 -0
- package/src/model/SoftDeletes.ts +3 -3
- package/src/model/State.ts +3 -3
- package/src/model/decorators/column.ts +17 -1
- package/src/model/decorators/table.ts +2 -2
- package/src/model/mixins.ts +126 -501
- package/src/schema/ModelInspector.ts +30 -3
package/src/model/State.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// workflow carry the API — `transitionTo` / `forceState` / `onTransition` / the
|
|
5
5
|
// `states` + `stateField` statics never appear on models that don't use them.
|
|
6
6
|
//
|
|
7
|
-
// import {
|
|
7
|
+
// import { Model, State } from "@zerotal/orm";
|
|
8
8
|
//
|
|
9
9
|
// const States = {
|
|
10
10
|
// pending: { canTransitionTo: ["active", "cancelled"] as const },
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// cancelled: { canTransitionTo: [] as const },
|
|
15
15
|
// } as const;
|
|
16
16
|
//
|
|
17
|
-
// class Subscription extends
|
|
17
|
+
// class Subscription extends Model.using(State) {
|
|
18
18
|
// static states = States;
|
|
19
19
|
// @column() status!: keyof typeof States;
|
|
20
20
|
// }
|
|
@@ -132,7 +132,7 @@ interface StateModelClass {
|
|
|
132
132
|
* cancelled: { canTransitionTo: [] as const },
|
|
133
133
|
* } as const;
|
|
134
134
|
*
|
|
135
|
-
* class Subscription extends
|
|
135
|
+
* class Subscription extends Model.using(State) {
|
|
136
136
|
* static states = States;
|
|
137
137
|
* @column() status!: keyof typeof States;
|
|
138
138
|
* }
|
|
@@ -29,6 +29,8 @@ export { columnRegistry };
|
|
|
29
29
|
* | `"date"` | `{ type: "datetime", cast: "date" }` |
|
|
30
30
|
* | `"json"` | `{ type: "json", cast: "json" }` |
|
|
31
31
|
* | `"array"` | `{ type: "json", cast: "array" }` |
|
|
32
|
+
* | `"encrypted"` | `{ type: "text", cast: "encrypted" }` |
|
|
33
|
+
* | `"encrypted:json"` | `{ type: "text", cast: "encrypted:json" }` |
|
|
32
34
|
*/
|
|
33
35
|
export type ColumnShorthand =
|
|
34
36
|
| "string"
|
|
@@ -40,7 +42,9 @@ export type ColumnShorthand =
|
|
|
40
42
|
| "datetime"
|
|
41
43
|
| "date"
|
|
42
44
|
| "json"
|
|
43
|
-
| "array"
|
|
45
|
+
| "array"
|
|
46
|
+
| "encrypted"
|
|
47
|
+
| "encrypted:json";
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
50
|
* Full option object accepted by `@column({ ... })`.
|
|
@@ -105,6 +109,12 @@ export interface ColumnOptions {
|
|
|
105
109
|
* - 'integer' — parseInt on both read and write
|
|
106
110
|
* - 'float' — parseFloat on both read and write
|
|
107
111
|
* - 'enum' — pass-through; pairs with `enumValues` for TS enum columns
|
|
112
|
+
* - 'encrypted' — AES-256-GCM at rest under `APP_KEY`, plaintext on the model
|
|
113
|
+
* - 'encrypted:json' — the same, for a structured value (stringified, then encrypted)
|
|
114
|
+
*
|
|
115
|
+
* Encrypted columns need `type: "text"` (a payload outgrows the plaintext) and
|
|
116
|
+
* cannot be filtered on — `where()` against one throws, because a fresh IV per
|
|
117
|
+
* write means the ciphertext never repeats. See `casts/encrypted.ts`.
|
|
108
118
|
*/
|
|
109
119
|
cast?:
|
|
110
120
|
| "datetime"
|
|
@@ -116,6 +126,8 @@ export interface ColumnOptions {
|
|
|
116
126
|
| "float"
|
|
117
127
|
| "enum"
|
|
118
128
|
| "immutable_datetime"
|
|
129
|
+
| "encrypted"
|
|
130
|
+
| "encrypted:json"
|
|
119
131
|
| `decimal:${number}`
|
|
120
132
|
| {
|
|
121
133
|
get?: (dbValue: unknown) => unknown;
|
|
@@ -140,6 +152,10 @@ const SHORTHAND_MAP: Record<ColumnShorthand, ColumnOptions> = {
|
|
|
140
152
|
date: { type: "datetime", cast: "date" },
|
|
141
153
|
json: { type: "json", cast: "json" },
|
|
142
154
|
array: { type: "json", cast: "array" },
|
|
155
|
+
// TEXT, not string: the stored payload is ~1.4× the plaintext plus 28 bytes of
|
|
156
|
+
// IV and auth tag, so a VARCHAR that held the value will not hold its ciphertext.
|
|
157
|
+
encrypted: { type: "text", cast: "encrypted" },
|
|
158
|
+
"encrypted:json": { type: "text", cast: "encrypted:json" },
|
|
143
159
|
};
|
|
144
160
|
|
|
145
161
|
/**
|
|
@@ -46,7 +46,7 @@ interface TableConfig {
|
|
|
46
46
|
* export class Post extends BaseModel { ... }
|
|
47
47
|
*
|
|
48
48
|
* Soft deletes are opt-in via the `SoftDeletes` mixin, not `@table`:
|
|
49
|
-
* `class Post extends
|
|
49
|
+
* `class Post extends Model.using(SoftDeletes) {}` — see /docs/orm/lifecycle.
|
|
50
50
|
*/
|
|
51
51
|
export interface TableDecoratorBuilder {
|
|
52
52
|
/** Apply the decorator to a class constructor (called automatically by TS). */
|
|
@@ -77,7 +77,7 @@ export interface TableDecoratorBuilder {
|
|
|
77
77
|
* ```
|
|
78
78
|
*
|
|
79
79
|
* Timestamps are **on by default** — use `.withoutTimestamps()` to opt out. Soft
|
|
80
|
-
* deletes are **opt-in via the `SoftDeletes` mixin**: `extends
|
|
80
|
+
* deletes are **opt-in via the `SoftDeletes` mixin**: `extends Model.using(SoftDeletes)`.
|
|
81
81
|
*
|
|
82
82
|
* `@table` is the single, required way to configure a model: besides setting the
|
|
83
83
|
* table name and options, it anchors the class's `@column`/relation registrations at
|