@antelopejs/interface-database-decorators 0.1.10 → 0.1.12
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/README.md +9 -7
- package/dist/common.js.map +1 -1
- package/dist/database.js +18 -2
- package/dist/database.js.map +1 -1
- package/dist/model.js +48 -29
- package/dist/model.js.map +1 -1
- package/dist/modifiers/common.d.ts +24 -4
- package/dist/modifiers/common.js +104 -130
- package/dist/modifiers/common.js.map +1 -1
- package/dist/modifiers/encryption.js +14 -9
- package/dist/modifiers/encryption.js.map +1 -1
- package/dist/modifiers/hash.d.ts +46 -4
- package/dist/modifiers/hash.js +172 -14
- package/dist/modifiers/hash.js.map +1 -1
- package/dist/modifiers/internal/kinds.d.ts +13 -0
- package/dist/modifiers/internal/kinds.js +33 -0
- package/dist/modifiers/internal/kinds.js.map +1 -0
- package/dist/modifiers/internal/records.d.ts +31 -0
- package/dist/modifiers/internal/records.js +56 -0
- package/dist/modifiers/internal/records.js.map +1 -0
- package/dist/modifiers/localization.js +4 -2
- package/dist/modifiers/localization.js.map +1 -1
- package/dist/schema.js +1 -2
- package/dist/schema.js.map +1 -1
- package/dist/table.js +21 -6
- package/dist/table.js.map +1 -1
- package/dist/tests/codec_helpers.d.ts +6 -2
- package/dist/tests/codec_helpers.js +18 -14
- package/dist/tests/codec_helpers.js.map +1 -1
- package/dist/tests/common.test.js +38 -46
- package/dist/tests/common.test.js.map +1 -1
- package/dist/tests/cross_instance.test.js +27 -2
- package/dist/tests/cross_instance.test.js.map +1 -1
- package/dist/tests/database.test.js +278 -256
- package/dist/tests/database.test.js.map +1 -1
- package/dist/tests/hash_upgrade.test.d.ts +1 -0
- package/dist/tests/hash_upgrade.test.js +132 -0
- package/dist/tests/hash_upgrade.test.js.map +1 -0
- package/dist/tests/helpers/hash-rows.d.ts +40 -0
- package/dist/tests/helpers/hash-rows.js +41 -0
- package/dist/tests/helpers/hash-rows.js.map +1 -0
- package/dist/tests/helpers/other-instance.d.ts +1 -1
- package/dist/tests/helpers/other-instance.js +41 -9
- package/dist/tests/helpers/other-instance.js.map +1 -1
- package/dist/tests/integration.test.js +485 -485
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/model.test.js +293 -308
- package/dist/tests/model.test.js.map +1 -1
- package/dist/tests/modifiers/autodate.test.js +80 -86
- package/dist/tests/modifiers/autodate.test.js.map +1 -1
- package/dist/tests/modifiers/common.test.js +181 -195
- package/dist/tests/modifiers/common.test.js.map +1 -1
- package/dist/tests/modifiers/encryption.test.js +149 -155
- package/dist/tests/modifiers/encryption.test.js.map +1 -1
- package/dist/tests/modifiers/hash.test.js +386 -99
- package/dist/tests/modifiers/hash.test.js.map +1 -1
- package/dist/tests/modifiers/localization.test.js +186 -196
- package/dist/tests/modifiers/localization.test.js.map +1 -1
- package/dist/tests/partial_update.test.d.ts +1 -0
- package/dist/tests/partial_update.test.js +55 -0
- package/dist/tests/partial_update.test.js.map +1 -0
- package/dist/tests/relation.test.js +92 -97
- package/dist/tests/relation.test.js.map +1 -1
- package/dist/tests/schema.test.js +101 -106
- package/dist/tests/schema.test.js.map +1 -1
- package/dist/tests/table.test.js +124 -132
- package/dist/tests/table.test.js.map +1 -1
- package/docs/3.table-modifiers.md +36 -15
- package/package.json +14 -3
- package/skills/database-decorators-interface/SKILL.md +4 -2
- package/skills/database-decorators-interface/cases.json +71 -0
package/dist/tests/table.test.js
CHANGED
|
@@ -10,141 +10,133 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const chai_1 = require("chai");
|
|
13
|
-
const common_1 = require("
|
|
14
|
-
const table_1 = require("
|
|
15
|
-
const common_2 = require("
|
|
13
|
+
const common_1 = require("../modifiers/common");
|
|
14
|
+
const table_1 = require("../table");
|
|
15
|
+
const common_2 = require("../common");
|
|
16
16
|
const codec_helpers_1 = require("./codec_helpers");
|
|
17
17
|
describe("Table - decorators", () => {
|
|
18
|
-
it("creates table with default primary key", async () =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
it("creates table with default primary key", async () => {
|
|
19
|
+
class TestTable extends table_1.Table {
|
|
20
|
+
name;
|
|
21
|
+
}
|
|
22
|
+
__decorate([
|
|
23
|
+
(0, table_1.Index)(),
|
|
24
|
+
__metadata("design:type", String)
|
|
25
|
+
], TestTable.prototype, "name", void 0);
|
|
26
|
+
const metadata = (0, common_2.getMetadata)(TestTable, common_2.DatumStaticMetadata);
|
|
27
|
+
(0, chai_1.expect)(metadata.primary).to.equal("_id");
|
|
28
|
+
(0, chai_1.expect)(metadata.indexes.name).to.deep.equal(["name"]);
|
|
29
|
+
});
|
|
30
|
+
it("creates table with multiple indexes", async () => {
|
|
31
|
+
class TestTable extends table_1.Table {
|
|
32
|
+
email;
|
|
33
|
+
age;
|
|
34
|
+
}
|
|
35
|
+
__decorate([
|
|
36
|
+
(0, table_1.Index)(),
|
|
37
|
+
__metadata("design:type", String)
|
|
38
|
+
], TestTable.prototype, "email", void 0);
|
|
39
|
+
__decorate([
|
|
40
|
+
(0, table_1.Index)(),
|
|
41
|
+
__metadata("design:type", Number)
|
|
42
|
+
], TestTable.prototype, "age", void 0);
|
|
43
|
+
const metadata = (0, common_2.getMetadata)(TestTable, common_2.DatumStaticMetadata);
|
|
44
|
+
(0, chai_1.expect)(metadata.primary).to.equal("_id");
|
|
45
|
+
(0, chai_1.expect)(metadata.indexes.email).to.deep.equal(["email"]);
|
|
46
|
+
(0, chai_1.expect)(metadata.indexes.age).to.deep.equal(["age"]);
|
|
47
|
+
});
|
|
48
|
+
it("creates table with grouped indexes", async () => {
|
|
49
|
+
class TestTable extends table_1.Table {
|
|
50
|
+
name;
|
|
51
|
+
email;
|
|
52
|
+
age;
|
|
53
|
+
}
|
|
54
|
+
__decorate([
|
|
55
|
+
(0, table_1.Index)({ group: "user_search" }),
|
|
56
|
+
__metadata("design:type", String)
|
|
57
|
+
], TestTable.prototype, "name", void 0);
|
|
58
|
+
__decorate([
|
|
59
|
+
(0, table_1.Index)({ group: "user_search" }),
|
|
60
|
+
__metadata("design:type", String)
|
|
61
|
+
], TestTable.prototype, "email", void 0);
|
|
62
|
+
__decorate([
|
|
63
|
+
(0, table_1.Index)({ group: "age_range" }),
|
|
64
|
+
__metadata("design:type", Number)
|
|
65
|
+
], TestTable.prototype, "age", void 0);
|
|
66
|
+
const metadata = (0, common_2.getMetadata)(TestTable, common_2.DatumStaticMetadata);
|
|
67
|
+
(0, chai_1.expect)(metadata.primary).to.equal("_id");
|
|
68
|
+
(0, chai_1.expect)(metadata.indexes.user_search).to.deep.equal(["name", "email"]);
|
|
69
|
+
(0, chai_1.expect)(metadata.indexes.age_range).to.deep.equal(["age"]);
|
|
70
|
+
});
|
|
71
|
+
it("creates table with fixture data", async () => {
|
|
72
|
+
const testData = [
|
|
73
|
+
{ id: "1", name: "Test User 1" },
|
|
74
|
+
{ id: "2", name: "Test User 2" },
|
|
75
|
+
];
|
|
76
|
+
let TestTable = class TestTable extends table_1.Table {
|
|
77
|
+
name;
|
|
78
|
+
};
|
|
79
|
+
TestTable = __decorate([
|
|
80
|
+
(0, table_1.Fixture)(() => testData)
|
|
81
|
+
], TestTable);
|
|
82
|
+
const metadata = (0, common_2.getMetadata)(TestTable, common_2.DatumStaticMetadata);
|
|
83
|
+
(0, chai_1.expect)(metadata.generator).to.be.a("function");
|
|
84
|
+
(0, chai_1.expect)(metadata.primary).to.equal("_id");
|
|
85
|
+
});
|
|
86
|
+
it("combines table with mixins", async () => {
|
|
87
|
+
class TestMixin {
|
|
88
|
+
testMethodUnunvailable() {
|
|
89
|
+
return "This method should be undefined";
|
|
90
|
+
}
|
|
91
|
+
[common_1.MixinSymbol] = class {
|
|
92
|
+
testMethod() {
|
|
93
|
+
return "mixin method ok";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
class TestTable extends table_1.Table.with(TestMixin) {
|
|
98
|
+
name;
|
|
99
|
+
}
|
|
100
|
+
const instance = new TestTable();
|
|
101
|
+
(0, chai_1.expect)(instance).to.have.property("testMethod");
|
|
102
|
+
(0, chai_1.expect)(instance.testMethod()).to.equal("mixin method ok");
|
|
103
|
+
(0, chai_1.expect)(instance).to.not.have.property("testMethodUnunvailable");
|
|
104
|
+
(0, chai_1.expect)(instance).to.have.property("name");
|
|
105
|
+
(0, chai_1.expect)(instance).to.be.instanceOf(TestTable);
|
|
106
|
+
});
|
|
23
107
|
});
|
|
24
|
-
async function CreateTableWithDefaultPrimaryKeyTest() {
|
|
25
|
-
class TestTable extends table_1.Table {
|
|
26
|
-
name;
|
|
27
|
-
}
|
|
28
|
-
__decorate([
|
|
29
|
-
(0, table_1.Index)(),
|
|
30
|
-
__metadata("design:type", String)
|
|
31
|
-
], TestTable.prototype, "name", void 0);
|
|
32
|
-
const metadata = (0, common_2.getMetadata)(TestTable, common_2.DatumStaticMetadata);
|
|
33
|
-
(0, chai_1.expect)(metadata.primary).to.equal("_id");
|
|
34
|
-
(0, chai_1.expect)(metadata.indexes.name).to.deep.equal(["name"]);
|
|
35
|
-
}
|
|
36
|
-
async function CreateTableWithMultipleIndexesTest() {
|
|
37
|
-
class TestTable extends table_1.Table {
|
|
38
|
-
email;
|
|
39
|
-
age;
|
|
40
|
-
}
|
|
41
|
-
__decorate([
|
|
42
|
-
(0, table_1.Index)(),
|
|
43
|
-
__metadata("design:type", String)
|
|
44
|
-
], TestTable.prototype, "email", void 0);
|
|
45
|
-
__decorate([
|
|
46
|
-
(0, table_1.Index)(),
|
|
47
|
-
__metadata("design:type", Number)
|
|
48
|
-
], TestTable.prototype, "age", void 0);
|
|
49
|
-
const metadata = (0, common_2.getMetadata)(TestTable, common_2.DatumStaticMetadata);
|
|
50
|
-
(0, chai_1.expect)(metadata.primary).to.equal("_id");
|
|
51
|
-
(0, chai_1.expect)(metadata.indexes.email).to.deep.equal(["email"]);
|
|
52
|
-
(0, chai_1.expect)(metadata.indexes.age).to.deep.equal(["age"]);
|
|
53
|
-
}
|
|
54
|
-
async function CreateTableWithGroupedIndexesTest() {
|
|
55
|
-
class TestTable extends table_1.Table {
|
|
56
|
-
name;
|
|
57
|
-
email;
|
|
58
|
-
age;
|
|
59
|
-
}
|
|
60
|
-
__decorate([
|
|
61
|
-
(0, table_1.Index)({ group: "user_search" }),
|
|
62
|
-
__metadata("design:type", String)
|
|
63
|
-
], TestTable.prototype, "name", void 0);
|
|
64
|
-
__decorate([
|
|
65
|
-
(0, table_1.Index)({ group: "user_search" }),
|
|
66
|
-
__metadata("design:type", String)
|
|
67
|
-
], TestTable.prototype, "email", void 0);
|
|
68
|
-
__decorate([
|
|
69
|
-
(0, table_1.Index)({ group: "age_range" }),
|
|
70
|
-
__metadata("design:type", Number)
|
|
71
|
-
], TestTable.prototype, "age", void 0);
|
|
72
|
-
const metadata = (0, common_2.getMetadata)(TestTable, common_2.DatumStaticMetadata);
|
|
73
|
-
(0, chai_1.expect)(metadata.primary).to.equal("_id");
|
|
74
|
-
(0, chai_1.expect)(metadata.indexes.user_search).to.deep.equal(["name", "email"]);
|
|
75
|
-
(0, chai_1.expect)(metadata.indexes.age_range).to.deep.equal(["age"]);
|
|
76
|
-
}
|
|
77
|
-
async function CreateTableWithFixtureDataTest() {
|
|
78
|
-
const testData = [
|
|
79
|
-
{ id: "1", name: "Test User 1" },
|
|
80
|
-
{ id: "2", name: "Test User 2" },
|
|
81
|
-
];
|
|
82
|
-
let TestTable = class TestTable extends table_1.Table {
|
|
83
|
-
name;
|
|
84
|
-
};
|
|
85
|
-
TestTable = __decorate([
|
|
86
|
-
(0, table_1.Fixture)(() => testData)
|
|
87
|
-
], TestTable);
|
|
88
|
-
const metadata = (0, common_2.getMetadata)(TestTable, common_2.DatumStaticMetadata);
|
|
89
|
-
(0, chai_1.expect)(metadata.generator).to.be.a("function");
|
|
90
|
-
(0, chai_1.expect)(metadata.primary).to.equal("_id");
|
|
91
|
-
}
|
|
92
108
|
describe("Table - @Field decorator", () => {
|
|
93
|
-
it("stores a string-token type on metadata.fields", () =>
|
|
94
|
-
|
|
95
|
-
it("stacks with @Index", () => StacksWithIndexTest());
|
|
96
|
-
});
|
|
97
|
-
function StoresStringTokenFieldTest() {
|
|
98
|
-
class _T extends table_1.Table {
|
|
99
|
-
}
|
|
100
|
-
__decorate([
|
|
101
|
-
(0, table_1.Field)("string"),
|
|
102
|
-
__metadata("design:type", String)
|
|
103
|
-
], _T.prototype, "name", void 0);
|
|
104
|
-
const metadata = (0, common_2.getMetadata)(_T, common_2.DatumStaticMetadata);
|
|
105
|
-
(0, chai_1.expect)(metadata.fields.name).to.equal("string");
|
|
106
|
-
}
|
|
107
|
-
function StoresCodecFieldTest() {
|
|
108
|
-
class _T extends table_1.Table {
|
|
109
|
-
}
|
|
110
|
-
__decorate([
|
|
111
|
-
(0, table_1.Field)((0, codec_helpers_1.asFieldType)(codec_helpers_1.stringCodec)),
|
|
112
|
-
__metadata("design:type", String)
|
|
113
|
-
], _T.prototype, "name", void 0);
|
|
114
|
-
const metadata = (0, common_2.getMetadata)(_T, common_2.DatumStaticMetadata);
|
|
115
|
-
(0, chai_1.expect)(metadata.fields.name).to.equal(codec_helpers_1.stringCodec);
|
|
116
|
-
}
|
|
117
|
-
function StacksWithIndexTest() {
|
|
118
|
-
class _T extends table_1.Table {
|
|
119
|
-
}
|
|
120
|
-
__decorate([
|
|
121
|
-
(0, table_1.Index)(),
|
|
122
|
-
(0, table_1.Field)("string"),
|
|
123
|
-
__metadata("design:type", String)
|
|
124
|
-
], _T.prototype, "email", void 0);
|
|
125
|
-
const metadata = (0, common_2.getMetadata)(_T, common_2.DatumStaticMetadata);
|
|
126
|
-
(0, chai_1.expect)(metadata.fields.email).to.equal("string");
|
|
127
|
-
(0, chai_1.expect)(metadata.indexes.email).to.deep.equal(["email"]);
|
|
128
|
-
}
|
|
129
|
-
async function CombineTableWithMixinsTest() {
|
|
130
|
-
class TestMixin {
|
|
131
|
-
testMethodUnunvailable() {
|
|
132
|
-
return "This method should be undefined";
|
|
109
|
+
it("stores a string-token type on metadata.fields", () => {
|
|
110
|
+
class _T extends table_1.Table {
|
|
133
111
|
}
|
|
134
|
-
[
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
112
|
+
__decorate([
|
|
113
|
+
(0, table_1.Field)("string"),
|
|
114
|
+
__metadata("design:type", String)
|
|
115
|
+
], _T.prototype, "name", void 0);
|
|
116
|
+
const metadata = (0, common_2.getMetadata)(_T, common_2.DatumStaticMetadata);
|
|
117
|
+
(0, chai_1.expect)(metadata.fields.name).to.equal("string");
|
|
118
|
+
});
|
|
119
|
+
it("stores an io-ts codec on metadata.fields", () => {
|
|
120
|
+
class _T extends table_1.Table {
|
|
121
|
+
}
|
|
122
|
+
__decorate([
|
|
123
|
+
(0, table_1.Field)((0, codec_helpers_1.asFieldType)(codec_helpers_1.stringCodec)),
|
|
124
|
+
__metadata("design:type", String)
|
|
125
|
+
], _T.prototype, "name", void 0);
|
|
126
|
+
const metadata = (0, common_2.getMetadata)(_T, common_2.DatumStaticMetadata);
|
|
127
|
+
(0, chai_1.expect)(metadata.fields.name).to.equal(codec_helpers_1.stringCodec);
|
|
128
|
+
});
|
|
129
|
+
it("stacks with @Index", () => {
|
|
130
|
+
class _T extends table_1.Table {
|
|
131
|
+
}
|
|
132
|
+
__decorate([
|
|
133
|
+
(0, table_1.Index)(),
|
|
134
|
+
(0, table_1.Field)("string"),
|
|
135
|
+
__metadata("design:type", String)
|
|
136
|
+
], _T.prototype, "email", void 0);
|
|
137
|
+
const metadata = (0, common_2.getMetadata)(_T, common_2.DatumStaticMetadata);
|
|
138
|
+
(0, chai_1.expect)(metadata.fields.email).to.equal("string");
|
|
139
|
+
(0, chai_1.expect)(metadata.indexes.email).to.deep.equal(["email"]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
150
142
|
//# sourceMappingURL=table.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.test.js","sourceRoot":"","sources":["../../src/tests/table.test.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,+BAA8B;AAC9B,
|
|
1
|
+
{"version":3,"file":"table.test.js","sourceRoot":"","sources":["../../src/tests/table.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -59,7 +59,7 @@ class UserCredentials extends Table.with(EncryptionModifier) {
|
|
|
59
59
|
|
|
60
60
|
### HashModifier
|
|
61
61
|
|
|
62
|
-
The `HashModifier` stores a salted one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value.
|
|
62
|
+
The `HashModifier` stores a salted one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value. By default it stores a scrypt hash, a memory-hard function fit for passwords.
|
|
63
63
|
|
|
64
64
|
```typescript
|
|
65
65
|
import {
|
|
@@ -73,39 +73,60 @@ class User extends Table.with(HashModifier) {
|
|
|
73
73
|
@Field("string")
|
|
74
74
|
declare email: string;
|
|
75
75
|
|
|
76
|
-
@Hashed(
|
|
76
|
+
@Hashed()
|
|
77
77
|
@Field("string")
|
|
78
78
|
declare password: string;
|
|
79
79
|
|
|
80
|
-
verifyPassword(plainPassword: string): boolean {
|
|
81
|
-
return this.
|
|
80
|
+
verifyPassword(plainPassword: string): Promise<boolean> {
|
|
81
|
+
return this.testHashAsync("password", plainPassword);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
#### How It Works
|
|
87
87
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
88
|
+
- Each write generates a random salt and stores `$scrypt$ln=15,r=8,p=3$<salt>$<key>` in the field. The string holds the algorithm, the cost, the salt and the hash, so the cost can change later.
|
|
89
|
+
- Assigning the stored hash back to the field, or calling `lock` on a loaded row, keeps the hash as it is.
|
|
90
|
+
- `testHash` and `testHashAsync` read the cost and the salt from the stored string, hash the candidate and compare in constant time.
|
|
91
|
+
- A test spends one scrypt run whatever the row holds, so the time of a refusal does not tell whether a hash exists.
|
|
92
|
+
- A write is synchronous, because a property setter cannot wait. Assigning a value blocks the event loop for one scrypt run, about 150 ms and 32 MiB.
|
|
91
93
|
|
|
92
94
|
#### Options
|
|
93
95
|
|
|
94
|
-
| Option | Type | Default
|
|
95
|
-
| ----------- | -------- |
|
|
96
|
-
| `algorithm` | `string` | `
|
|
97
|
-
|
|
98
|
-
`LocalizationModifier.fallbackLocaleOf(TableClass, field)` returns a field's `fallbackLocale`, or `undefined` when the field is not localized or declares none. Generated forms use it to require a value in that locale.
|
|
96
|
+
| Option | Type | Default | Description |
|
|
97
|
+
| ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `algorithm` | `string` | none | Digest to store instead of scrypt, any name `createHash` accepts. A digest is fast to guess: use it only for non-secret fingerprints, never for credentials. Rows stay in the bare digest format. |
|
|
99
99
|
|
|
100
|
-
#### Mixin
|
|
100
|
+
#### Mixin Methods
|
|
101
101
|
|
|
102
|
-
The `HashModifier` mixin adds
|
|
102
|
+
The `HashModifier` mixin adds these methods to table instances:
|
|
103
103
|
|
|
104
104
|
```typescript
|
|
105
105
|
testHash(field: string, value: unknown): boolean
|
|
106
|
+
testHashAsync(field: string, value: unknown): Promise<boolean>
|
|
107
|
+
isHashOutdated(field: string): boolean
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- `testHashAsync` returns whether `value` matches the stored hash. It runs scrypt on the thread pool, so use it on a server.
|
|
111
|
+
- `testHash` returns the same result and blocks the event loop while scrypt runs.
|
|
112
|
+
- `isHashOutdated` returns whether the stored hash differs from what the field writes today. It is always `false` on a field with an `algorithm`, and when nothing is stored.
|
|
113
|
+
|
|
114
|
+
#### Upgrade Stored Hashes
|
|
115
|
+
|
|
116
|
+
A row written before scrypt became the default holds a bare salted SHA-256. It still verifies, and `isHashOutdated` returns `true` for it. The same applies to a hash of an earlier cost. To upgrade a row, assign the value after a successful test, because that is the only moment the plaintext is known:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
if (await user.testHashAsync("password", password)) {
|
|
120
|
+
if (user.isHashOutdated("password")) {
|
|
121
|
+
user.password = password;
|
|
122
|
+
await userModel.update(user);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
106
125
|
```
|
|
107
126
|
|
|
108
|
-
|
|
127
|
+
To move a credential off `@Hashed({ algorithm: "sha256" })`, remove the option. Its rows keep verifying and are flagged for upgrade. A row written with another digest verifies only while the field declares that digest.
|
|
128
|
+
|
|
129
|
+
`LocalizationModifier.fallbackLocaleOf(TableClass, field)` returns a field's `fallbackLocale`, or `undefined` when the field is not localized or declares none. Generated forms use it to require a value in that locale.
|
|
109
130
|
|
|
110
131
|
### LocalizationModifier
|
|
111
132
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-database-decorators",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "AntelopeJS decorators for database tables and typed data models, with encrypted, hashed and localized fields",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -127,7 +127,17 @@
|
|
|
127
127
|
"release": "pnpm run lint && pnpm run prepack && release-it",
|
|
128
128
|
"test": "pnpm run build && ajs module test .",
|
|
129
129
|
"format:check": "oxfmt --check .",
|
|
130
|
-
"knip": "knip"
|
|
130
|
+
"knip": "knip",
|
|
131
|
+
"prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
|
|
132
|
+
"checks": "antelopejs-checks",
|
|
133
|
+
"size": "antelopejs-check-size",
|
|
134
|
+
"ratchet": "antelopejs-ratchet",
|
|
135
|
+
"sync": "antelopejs-sync",
|
|
136
|
+
"ratchet:update": "antelopejs-ratchet --update",
|
|
137
|
+
"sync:check": "antelopejs-sync --check",
|
|
138
|
+
"typecheck": "tsc --noEmit",
|
|
139
|
+
"suppressions": "antelopejs-suppressions",
|
|
140
|
+
"suppressions:update": "antelopejs-suppressions --update"
|
|
131
141
|
},
|
|
132
142
|
"dependencies": {
|
|
133
143
|
"randomstring": "^1.3.1",
|
|
@@ -138,7 +148,7 @@
|
|
|
138
148
|
"@antelopejs/interface-api": "^0.0.15",
|
|
139
149
|
"@antelopejs/interface-core": "^0.2.4",
|
|
140
150
|
"@antelopejs/interface-database": "^0.1.11",
|
|
141
|
-
"@antelopejs/tooling-configs": "^0.
|
|
151
|
+
"@antelopejs/tooling-configs": "^0.2.17",
|
|
142
152
|
"@types/chai": "^4.3.20",
|
|
143
153
|
"@types/mocha": "^10.0.10",
|
|
144
154
|
"@types/node": "^22.19.15",
|
|
@@ -147,6 +157,7 @@
|
|
|
147
157
|
"chai": "^4.5.0",
|
|
148
158
|
"eslint-plugin-perfectionist": "^5.11.0",
|
|
149
159
|
"knip": "^6.34.0",
|
|
160
|
+
"lefthook": "^2.2.2",
|
|
150
161
|
"mongodb-memory-server-core": "^10.4.3",
|
|
151
162
|
"oxfmt": "^0.66.0",
|
|
152
163
|
"oxlint": "1.81.0",
|
|
@@ -110,7 +110,7 @@ class Chef extends Table.with(
|
|
|
110
110
|
EncryptionModifier,
|
|
111
111
|
LocalizationModifier,
|
|
112
112
|
) {
|
|
113
|
-
@Hashed() declare passphrase: string; // one-way; test via instance.
|
|
113
|
+
@Hashed() declare passphrase: string; // one-way scrypt; test via await instance.testHashAsync("passphrase", value)
|
|
114
114
|
@Encrypted({ secretKey }) declare supplierCode: string; // transparent encrypt/decrypt (autolock + autounlock)
|
|
115
115
|
@Localized() declare motto: string; // read AND write via instance.localize("en", ["motto"])
|
|
116
116
|
@CreationTime() declare hiredAt: Date; // set on insert, stripped from updates
|
|
@@ -124,7 +124,9 @@ class Chef extends Table.with(
|
|
|
124
124
|
is loaded once before decorators run (e.g. `import "@antelopejs/interface-database-decorators"`).
|
|
125
125
|
- `Hashed`, `Encrypted`, and `Localized` require the matching modifier class in `Table.with(...)`; `CreationTime`/`UpdateTime` are event-only and need no mixin.
|
|
126
126
|
- Modifier ordering matters: `attachModifier` throws if you stack a modifier after a one-way modifier (e.g. anything after `@Hashed` on the same field).
|
|
127
|
-
- Hashed fields cannot be read back or queried directly — only equality-tested with `testHash
|
|
127
|
+
- Hashed fields cannot be read back or queried directly — only equality-tested with `testHashAsync`, or `testHash`, which blocks the event loop for one scrypt run.
|
|
128
|
+
- A row can hold an older hash that still verifies. After a successful test, when `instance.isHashOutdated(field)` is true, assign the tested value and save the row.
|
|
129
|
+
- `@Hashed({ algorithm })` stores a fast digest instead of scrypt: use it only for non-secret fingerprints, never for credentials.
|
|
128
130
|
- Localized fields must be written through a localized instance (`instance.localize(locale).field = ...`);
|
|
129
131
|
values assigned before `localize()` are held in floating state and silently dropped on insert/update.
|
|
130
132
|
- A localized value `""` is stored as no value: reads and queries in that locale get the `fallbackLocale` value.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"cases": [
|
|
4
|
+
{
|
|
5
|
+
"id": "table-relations",
|
|
6
|
+
"area": "framework",
|
|
7
|
+
"style": "clean",
|
|
8
|
+
"prompt": "How do I define relations between tables?",
|
|
9
|
+
"origin": "docs-assistant",
|
|
10
|
+
"expect": {
|
|
11
|
+
"skills": ["database-decorators-interface"],
|
|
12
|
+
"acceptable": ["database-interface"],
|
|
13
|
+
"mustMention": ["Relation"]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "table-relations-casual",
|
|
18
|
+
"area": "framework",
|
|
19
|
+
"style": "casual",
|
|
20
|
+
"prompt": "how do i link two tables together",
|
|
21
|
+
"origin": "docs-assistant",
|
|
22
|
+
"expect": {
|
|
23
|
+
"skills": ["database-decorators-interface"],
|
|
24
|
+
"acceptable": ["database-interface"],
|
|
25
|
+
"mustMention": ["Relation"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "database-decorators-interface-aql-neighbour",
|
|
30
|
+
"area": "framework",
|
|
31
|
+
"style": "clean",
|
|
32
|
+
"prompt": "Write a query that returns the novels with at least 300 pages, sorted by page count descending, and watch the table for changes.",
|
|
33
|
+
"expect": {
|
|
34
|
+
"skills": ["database-interface"],
|
|
35
|
+
"acceptable": ["antelopejs", "ajs-consume-interface"],
|
|
36
|
+
"avoid": ["database-decorators-interface"]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "database-decorators-interface-modifiers",
|
|
41
|
+
"area": "framework",
|
|
42
|
+
"style": "clean",
|
|
43
|
+
"prompt": "On my user table class, store the password so it can only be verified, encrypt the API key transparently, and keep created and updated timestamps.",
|
|
44
|
+
"expect": {
|
|
45
|
+
"skills": ["database-decorators-interface"],
|
|
46
|
+
"acceptable": [
|
|
47
|
+
"database-interface",
|
|
48
|
+
"antelopejs",
|
|
49
|
+
"ajs-consume-interface"
|
|
50
|
+
],
|
|
51
|
+
"mustMention": [
|
|
52
|
+
"Hashed",
|
|
53
|
+
"Encrypted",
|
|
54
|
+
"HashModifier",
|
|
55
|
+
"EncryptionModifier"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "database-decorators-interface-fixture-casual",
|
|
61
|
+
"area": "framework",
|
|
62
|
+
"style": "casual",
|
|
63
|
+
"prompt": "how do i seed some default rows when the table is empty",
|
|
64
|
+
"expect": {
|
|
65
|
+
"skills": ["database-decorators-interface"],
|
|
66
|
+
"acceptable": ["database-interface", "antelopejs"],
|
|
67
|
+
"mustMention": ["Fixture", "RegisterSchema"]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|