Skip to content

Commit e77b30e

Browse files
author
Jeffrey Young
committed
add migrate
1 parent 6f5acf7 commit e77b30e

14 files changed

Lines changed: 360 additions & 150 deletions

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ module.exports = {
66
// Optional (but recommended)
77
"jest-dynalite/clearAfterEach",
88
],
9-
9+
testTimeout: 20000,
1010
//preset: "jest-dynalite"
1111
};

src/batch-args-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
FieldsToProject,
3-
getDefaultFieldsToProject,
3+
getAllProjectableFields,
44
} from "./utils/ProjectFields";
55
import { GetRequest } from "./batch-get";
66
import { DeleteRequest, PutRequest } from "./batch-write";
@@ -41,7 +41,7 @@ export class BatchArgsHandler<
4141
Key: this.mapper.getKey(item as any),
4242
projectionFields:
4343
extraArgs?.fieldsToProject ||
44-
getDefaultFieldsToProject(this.mapper.args as any),
44+
getAllProjectableFields(this.mapper.args as any),
4545
};
4646
}
4747

src/mapper.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ export type RepositoryArgs<
8686
primaryIndex: IndexBase<T, PrimaryKeyField> & {
8787
tag?: IndexTag;
8888
};
89+
/**
90+
* Called when zod throws a validation error after retrieving an object
91+
* from the database. In migrate, an attempt should be made to fix the
92+
* error and write the object back to the database.
93+
*/
94+
migrate?: (rawObjectRetrievedFromDb: unknown) => Promise<T>;
8995
secondaryIndexes?: Record<
9096
SecondaryIndexTag,
9197
IndexBase<T> & SecondaryIndex<T>
@@ -170,7 +176,10 @@ export class Mapper<
170176
type === "input"
171177
? "single-table-InputValidationError"
172178
: "single-table-OutputValidationError",
173-
message: `Invalid ${this.args.typeName} ${type}`,
179+
message:
180+
type === "input"
181+
? `Recieved invalid data for ${this.args.typeName}`
182+
: `Invalid data for ${this.args.typeName} is stored in the database`,
174183
});
175184
}
176185
}
@@ -285,11 +294,9 @@ export class Mapper<
285294
[...pkFields, ...skFields].forEach((f) => {
286295
if (!Object(thing).hasOwnProperty(f)) {
287296
throw new Error(
288-
`To query index: ${JSON.stringify(
289-
index
290-
)}, field: ${f} is required, recieved ${JSON.stringify(
291-
thing
292-
)}, debugInfo: ${JSON.stringify(options?.debugInfo || {})}`
297+
`To query index (${index.pk}, ${
298+
index.sk
299+
}), field: ${f} is required, recieved ${JSON.stringify(thing)}`
293300
);
294301
}
295302
});

src/repository.ts

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import { BatchArgsHandler } from "./batch-args-handler";
66
import {
77
FieldsToProject,
88
toProjectionExpression,
9-
getDefaultFieldsToProject,
9+
getAllProjectableFields,
1010
} from "./utils/ProjectFields";
1111
import { createSTDError, isSingleTableDynamoError } from "./utils/errors";
1212
import { z } from "zod";
13+
import { goTry } from "./utils/goTry";
1314

14-
type ExtraQueryParams<T> = {
15+
type GetOptions<T> = {
1516
fieldsToProject: FieldsToProject<T>;
1617
};
1718

@@ -58,34 +59,64 @@ export class Repository<
5859
this.ddb = ddb;
5960
}
6061

61-
private doGet(id: ID, extraParams: ExtraQueryParams<Output>) {
62+
private async doGet(
63+
id: ID,
64+
options: GetOptions<Output>
65+
): Promise<Output | null> {
6266
const args = {
6367
TableName: this.args.tableName,
64-
...toProjectionExpression(extraParams.fieldsToProject),
68+
...toProjectionExpression(options.fieldsToProject),
6569
Key: this.mapper.getKey(id),
6670
};
6771

68-
return this.args.getDocument
72+
const res = await (this.args.getDocument
6973
? this.args.getDocument(args)
70-
: this.ddb.get(args).promise();
74+
: this.ddb.get(args).promise());
75+
76+
if (!res.Item) {
77+
return null;
78+
}
79+
const [item, err] = goTry(() =>
80+
this.mapper.pickedParse(res.Item, options.fieldsToProject, "output")
81+
);
82+
// we got a parse error :O
83+
if (err) {
84+
if (this.args.migrate) {
85+
if (this.isProjectingAllFields(options.fieldsToProject)) {
86+
// if this is the full version that's in the db
87+
return this.args.migrate(res.Item);
88+
} else {
89+
const fullMigratedResult = await this.doGet(id, {
90+
fieldsToProject: getAllProjectableFields(this.args),
91+
});
92+
93+
return this.mapper.pickedParse(
94+
fullMigratedResult,
95+
options.fieldsToProject,
96+
"output"
97+
) as any;
98+
}
99+
}
100+
throw err;
101+
}
102+
return item as Output;
103+
}
104+
105+
isProjectingAllFields(fieldsToProject: FieldsToProject<Output>) {
106+
const s = new Set(fieldsToProject);
107+
const allFields = getAllProjectableFields(this.args);
108+
return allFields.every((f) => s.has(f));
71109
}
72110

73111
async get(
74112
id: ID,
75-
extraParams: ExtraQueryParams<Output> = {
76-
fieldsToProject: getDefaultFieldsToProject<Output>(this.args as any),
113+
extraParams: GetOptions<Output> = {
114+
fieldsToProject: getAllProjectableFields(this.args),
77115
}
78116
) {
79117
try {
80-
const res = await this.doGet(id, extraParams);
118+
const item: Output | null = await this.doGet(id, extraParams);
81119

82-
let item: Output | null = res.Item
83-
? (this.mapper.pickedParse(
84-
res.Item,
85-
extraParams.fieldsToProject,
86-
"output"
87-
) as any)
88-
: null;
89120
this.args.on?.get?.(
90121
[id, extraParams as any],
91122
item,
@@ -284,7 +315,7 @@ export class Repository<
284315
tableName: this.args.tableName,
285316
index: this.getIndexByTag(indexTag),
286317
mapper: this.mapper as any,
287-
fieldsToProject: getDefaultFieldsToProject(this.args),
318+
fieldsToProject: getAllProjectableFields(this.args),
288319
ddb: this.ddb,
289320
});
290321
return builder;

src/test/errors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Object {
8484
test("output error should be thrown", async () => {
8585
await rawPut({ id: "meow", invalidField: "wat" });
8686
expect(getUserRepo().get({ id: "meow" })).rejects.toMatchObject({
87-
message: "Invalid User output",
87+
message: "Invalid data for User is stored in the database",
8888
name: "single-table-OutputValidationError",
8989
cause: {
9090
issues: [

src/test/index-query-builder.test.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,13 @@ test("should build query with sortkey", () => {
140140

141141
test("should throw with no partition key", () => {
142142
expect(() =>
143-
getBuilder(mapper.args.primaryIndex)
144-
.where({
145-
state: "UT",
146-
})
147-
.limit(25)
148-
.build()
149-
).toThrowErrorMatchingInlineSnapshot(
150-
`"To query index: {\\"pk\\":\\"pk1\\",\\"sk\\":\\"sk1\\",\\"tag\\":\\"countryByStateByCreatedAt\\",\\"fields\\":[\\"country\\",\\"state\\",\\"createdAt\\"]}, field: country is required, recieved {\\"state\\":\\"UT\\"}, debugInfo: {}"`
151-
);
143+
getBuilder(mapper.args.primaryIndex).
144+
where({
145+
state: "UT" }).
146+
147+
limit(25).
148+
build()).
149+
toThrowErrorMatchingInlineSnapshot(`"To query index (pk1, sk1), field: country is required, recieved {\\"state\\":\\"UT\\"}"`);
152150
});
153151

154152
test("should build non primary index", () => {

src/test/integration/hooks.test.ts

Lines changed: 68 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -69,78 +69,78 @@ test("hooks should get called", async () => {
6969
]
7070
`);
7171
expect(spies.get.getCalls().map((c) => c.args)).toMatchInlineSnapshot(`
72+
Array [
73+
Array [
7274
Array [
73-
Array [
74-
Array [
75-
Object {
76-
"id": "1",
77-
},
78-
Object {
79-
"fieldsToProject": Array [
80-
"id",
81-
"name",
82-
],
83-
},
75+
Object {
76+
"id": "1",
77+
},
78+
Object {
79+
"fieldsToProject": Array [
80+
"id",
81+
"name",
8482
],
85-
Object {
86-
"id": "1",
87-
"name": "meow",
88-
},
89-
Object {
90-
"Key": Object {
91-
"pk1": "Thing#1",
92-
"sk1": "Thing",
93-
},
94-
"TableName": "table1",
95-
},
96-
],
97-
Array [
98-
Array [
99-
Object {
100-
"id": "1",
101-
},
102-
Object {
103-
"fieldsToProject": Array [
104-
"id",
105-
"name",
106-
],
107-
},
83+
},
84+
],
85+
Object {
86+
"id": "1",
87+
"name": "meow",
88+
},
89+
Object {
90+
"Key": Object {
91+
"pk1": "Thing#1",
92+
"sk1": "Thing",
93+
},
94+
"TableName": "table1",
95+
},
96+
],
97+
Array [
98+
Array [
99+
Object {
100+
"id": "1",
101+
},
102+
Object {
103+
"fieldsToProject": Array [
104+
"id",
105+
"name",
108106
],
109-
Object {
110-
"id": "1",
111-
"name": "yeehaw",
112-
},
113-
Object {
114-
"Key": Object {
115-
"pk1": "Thing#1",
116-
"sk1": "Thing",
117-
},
118-
"TableName": "table1",
119-
},
120-
],
121-
Array [
122-
Array [
123-
Object {
124-
"id": "1",
125-
},
126-
Object {
127-
"fieldsToProject": Array [
128-
"id",
129-
"name",
130-
],
131-
},
107+
},
108+
],
109+
Object {
110+
"id": "1",
111+
"name": "yeehaw",
112+
},
113+
Object {
114+
"Key": Object {
115+
"pk1": "Thing#1",
116+
"sk1": "Thing",
117+
},
118+
"TableName": "table1",
119+
},
120+
],
121+
Array [
122+
Array [
123+
Object {
124+
"id": "1",
125+
},
126+
Object {
127+
"fieldsToProject": Array [
128+
"id",
129+
"name",
132130
],
133-
null,
134-
Object {
135-
"Key": Object {
136-
"pk1": "Thing#1",
137-
"sk1": "Thing",
138-
},
139-
"TableName": "table1",
140-
},
141-
],
142-
]
143-
`);
131+
},
132+
],
133+
null,
134+
Object {
135+
"Key": Object {
136+
"pk1": "Thing#1",
137+
"sk1": "Thing",
138+
},
139+
"TableName": "table1",
140+
},
141+
],
142+
]
143+
`);
144144
expect(spies.updateUnsafe.getCalls().map((c) => c.args))
145145
.toMatchInlineSnapshot(`
146146
Array [

src/test/integration/repository-integration.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,12 @@ test("get, put, delete, updateUnsafe, and query should work", async () => {
104104
`);
105105

106106
await expect(() =>
107-
repo.query("primary").where({ city: "scranton" }).exec()
108-
).toThrow();
107+
repo.query("primary").where({ city: "scranton" }).exec()).
108+
toThrowErrorMatchingInlineSnapshot(`"To query index (pk1, sk1), field: id is required, recieved {\\"city\\":\\"scranton\\"}"`);
109109

110110
await expect(() =>
111-
repo.query("byCountryByStateByCity").where({ city: "scranton" }).exec()
112-
).toThrow();
111+
repo.query("byCountryByStateByCity").where({ city: "scranton" }).exec()).
112+
toThrowErrorMatchingInlineSnapshot(`"To query index (pk2, sk2), field: country is required, recieved {\\"city\\":\\"scranton\\"}"`);
113113

114114
await expect(
115115
repo

src/test/mapper.test.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ test("should format partial index properly", () => {
119119

120120
test("should throw when no partition key is provided", () => {
121121
expect(() =>
122-
mapper.getIndexKey({} as any, mapper.args.primaryIndex, { partial: true })
123-
).toThrow();
122+
mapper.getIndexKey(({} as any), mapper.args.primaryIndex, { partial: true })).
123+
toThrowErrorMatchingInlineSnapshot(`"To query index (pk1, sk1), field: country is required, recieved {}"`);
124124
});
125125

126126
test("mapper.updateWithSomeFields should work", () => {
@@ -173,13 +173,11 @@ test("mapper.updateWithSomeFields should work", () => {
173173
});
174174

175175
expect(() =>
176-
mapper.decorateWithKeys({
177-
id: "1",
178-
country: "usa",
179-
} as any)
180-
).toThrowErrorMatchingInlineSnapshot(
181-
`"To query index: {\\"fields\\":[\\"country\\",\\"name\\"],\\"indexName\\":\\"index1\\",\\"pk\\":\\"pk1\\",\\"sk\\":\\"sk1\\"}, field: name is required, recieved {\\"id\\":\\"1\\",\\"country\\":\\"usa\\"}, debugInfo: {}"`
182-
);
176+
mapper.decorateWithKeys(({
177+
id: "1",
178+
country: "usa" } as
179+
any))).
180+
toThrowErrorMatchingInlineSnapshot(`"To query index (pk1, sk1), field: name is required, recieved {\\"id\\":\\"1\\",\\"country\\":\\"usa\\"}"`);
183181

184182
const fullObject = {
185183
birthDate: "1990",

0 commit comments

Comments
 (0)