Skip to content

Commit 66bfe31

Browse files
manueliglesiasiartemievdavid-mcafeedpilchsvidgen
committed
feat(datastore): custom pk support
Co-authored-by: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Co-authored-by: David McAfee <mcafd@amazon.com> Co-authored-by: Dane Pilcher <dppilche@amazon.com> Co-authored-by: Jon Wire <iambipedal@gmail.com>
1 parent 1fd1443 commit 66bfe31

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+5561
-1084
lines changed

.circleci/config.yml

+2
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ jobs:
369369
command: |
370370
cd packages/datastore-storage-adapter
371371
npm install --build-from-source
372+
rm -rf node_modules/@aws-amplify node_modules/@aws-sdk
372373
- run:
373374
name: 'Run Amplify JS unit tests'
374375
command: |
@@ -1218,6 +1219,7 @@ releasable_branches: &releasable_branches
12181219
- ui-components/main
12191220
- 1.0-stable
12201221
- geo/main
1222+
- ds-custom-pk
12211223

12221224
test_browsers: &test_browsers
12231225
browser: [chrome, firefox]

.envrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use node 14

.vscode/launch.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// A launch configuration that compiles the extension and then opens it inside a new window
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
{
6+
"version": "0.2.0",
7+
"configurations": [
8+
{
9+
"name": "debug tests",
10+
"type": "node",
11+
"request": "launch",
12+
// The debugger will only run tests for the package specified here:
13+
"cwd": "${workspaceFolder}/packages/datastore",
14+
"runtimeArgs": [
15+
"--inspect-brk",
16+
"${workspaceRoot}/node_modules/.bin/jest",
17+
// Optionally specify a single test file to run/debug:
18+
"storage.test.ts",
19+
"--runInBand",
20+
"--testTimeout",
21+
"600000", // 10 min timeout so jest doesn't error while we're stepping through code
22+
"false"
23+
],
24+
"console": "integratedTerminal",
25+
"internalConsoleOptions": "neverOpen"
26+
}
27+
]
28+
}

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
"publish:1.0-stable": "lerna publish --conventional-commits --yes --dist-tag=stable-1.0 --message 'chore(release): Publish [ci skip]' --no-verify-access",
2828
"publish:ui-components/main": "lerna publish --canary --force-publish \"*\" --yes --dist-tag=ui-preview --preid=ui-preview --exact --no-verify-access",
2929
"publish:verdaccio": "lerna publish --no-push --canary minor --dist-tag=unstable --preid=unstable --exact --force-publish --yes --no-verify-access",
30-
"publish:geo/main": "lerna publish --canary --force-publish \"*\" --yes --dist-tag=geo --preid=geo --exact --no-verify-access"
30+
"publish:geo/main": "lerna publish --canary --force-publish \"*\" --yes --dist-tag=geo --preid=geo --exact --no-verify-access",
31+
"publish:ds-custom-pk": "lerna publish --canary --force-publish \"*\" --yes --dist-tag=custom-pk --preid=custom-pk --exact --no-verify-access",
32+
"temp-ds-safe-push": "yarn build --scope @aws-amplify/datastore && yarn test --scope @aws-amplify/datastore && git push origin"
3133
},
3234
"husky": {
3335
"hooks": {

packages/datastore-storage-adapter/__tests__/SQLiteAdapter.test.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ class InnerSQLiteDatabase {
7676
statement,
7777
params,
7878
async (err, row) => {
79+
if (err) {
80+
console.error('SQLite ERROR', new Error(err));
81+
console.warn(statement, params);
82+
}
7983
rows.push(row);
8084
},
8185
() => {
@@ -86,7 +90,14 @@ class InnerSQLiteDatabase {
8690
if (callback) await callback(this, resultSet);
8791
});
8892
} else {
89-
return await this.innerDB.run(statement, params, callback);
93+
return await this.innerDB.run(statement, params, err => {
94+
if (typeof callback === 'function') {
95+
callback(err);
96+
} else if (err) {
97+
console.error('calback', err);
98+
throw err;
99+
}
100+
});
90101
}
91102
}
92103

packages/datastore-storage-adapter/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"es5",
5252
"es2015",
5353
"esnext.asynciterable",
54-
"es2019"
54+
"es2019",
55+
"dom"
5556
],
5657
"allowJs": true,
5758
"esModuleInterop": true,

packages/datastore-storage-adapter/src/common/CommonSQLiteAdapter.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
ModelSortPredicateCreator,
1818
InternalSchema,
1919
isPredicateObj,
20-
ModelInstanceMetadata,
2120
ModelPredicate,
2221
NamespaceResolver,
2322
OpType,
@@ -29,7 +28,7 @@ import {
2928
QueryOne,
3029
utils,
3130
} from '@aws-amplify/datastore';
32-
import { CommonSQLiteDatabase, ParameterizedStatement } from './types';
31+
import { CommonSQLiteDatabase, ParameterizedStatement, ModelInstanceMetadataWithId } from './types';
3332

3433
const { traverseModel, validatePredicate, isModelConstructor } = utils;
3534

@@ -407,7 +406,7 @@ export class CommonSQLiteAdapter implements StorageAdapter {
407406

408407
async batchSave<T extends PersistentModel>(
409408
modelConstructor: PersistentModelConstructor<any>,
410-
items: ModelInstanceMetadata[]
409+
items: ModelInstanceMetadataWithId[]
411410
): Promise<[T, OpType][]> {
412411
const { name: tableName } = modelConstructor;
413412
const result: [T, OpType][] = [];

packages/datastore-storage-adapter/src/common/SQLiteUtils.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export function modelCreateTableStatement(
148148
let fields = Object.values(model.fields).reduce((acc, field: ModelField) => {
149149
if (isGraphQLScalarType(field.type)) {
150150
if (field.name === 'id') {
151-
return acc + '"id" PRIMARY KEY NOT NULL';
151+
return [...acc, '"id" PRIMARY KEY NOT NULL'];
152152
}
153153

154154
let columnParam = `"${field.name}" ${getSQLiteType(field.type)}`;
@@ -157,7 +157,7 @@ export function modelCreateTableStatement(
157157
columnParam += ' NOT NULL';
158158
}
159159

160-
return acc + `, ${columnParam}`;
160+
return [...acc, `${columnParam}`];
161161
}
162162

163163
if (isModelFieldType(field.type)) {
@@ -167,7 +167,7 @@ export function modelCreateTableStatement(
167167
if (isTargetNameAssociation(field.association)) {
168168
// check if this field has been explicitly defined in the model
169169
const fkDefinedInModel = Object.values(model.fields).find(
170-
(f: ModelField) => f.name === field.association.targetName
170+
(f: ModelField) => f.name === field?.association?.targetName
171171
);
172172

173173
// if the FK is not explicitly defined in the model, we have to add it here
@@ -179,7 +179,7 @@ export function modelCreateTableStatement(
179179

180180
// ignore isRequired param for model fields, since they will not contain
181181
// the related data locally
182-
return acc + `, ${columnParam}`;
182+
return [...acc, `${columnParam}`];
183183
}
184184

185185
// default to TEXT
@@ -189,19 +189,25 @@ export function modelCreateTableStatement(
189189
columnParam += ' NOT NULL';
190190
}
191191

192-
return acc + `, ${columnParam}`;
193-
}, '');
192+
return [...acc, `${columnParam}`];
193+
}, [] as string[]);
194194

195195
implicitAuthFields.forEach((authField: string) => {
196-
fields += `, ${authField} TEXT`;
196+
fields.push(`${authField} TEXT`);
197197
});
198198

199199
if (userModel) {
200-
fields +=
201-
', "_version" INTEGER, "_lastChangedAt" INTEGER, "_deleted" INTEGER';
200+
fields = [
201+
...fields,
202+
`"_version" INTEGER`,
203+
`"_lastChangedAt" INTEGER`,
204+
`"_deleted" INTEGER`,
205+
];
202206
}
203207

204-
const createTableStatement = `CREATE TABLE IF NOT EXISTS "${model.name}" (${fields});`;
208+
const createTableStatement = `CREATE TABLE IF NOT EXISTS "${
209+
model.name
210+
}" (${fields.join(', ')});`;
205211
return createTableStatement;
206212
}
207213

packages/datastore-storage-adapter/src/common/types.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PersistentModel } from '@aws-amplify/datastore';
1+
import { PersistentModel, ModelInstanceMetadata } from '@aws-amplify/datastore';
22

33
export interface CommonSQLiteDatabase {
44
init(): Promise<void>;
@@ -27,3 +27,8 @@ export interface CommonSQLiteDatabase {
2727
}
2828

2929
export type ParameterizedStatement = [string, any[]];
30+
31+
// TODO: remove once we implement CPK for this adapter
32+
export type ModelInstanceMetadataWithId = ModelInstanceMetadata & {
33+
id: string;
34+
};

packages/datastore/__tests__/AsyncStorage.ts

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ describe('AsyncStorage tests', () => {
168168

169169
test('save function 1:1 insert', async () => {
170170
await DataStore.save(blog);
171+
171172
await DataStore.save(owner);
172173

173174
const get1 = JSON.parse(

packages/datastore/__tests__/AsyncStorageAdapter.test.ts

+55-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import {
55
syncClasses,
66
} from '../src/datastore/datastore';
77
import { PersistentModelConstructor, SortDirection } from '../src/types';
8-
import { pause, Model, User, Profile, testSchema } from './helpers';
8+
import {
9+
Model,
10+
User,
11+
Profile,
12+
Post,
13+
Comment,
14+
testSchema,
15+
pause,
16+
} from './helpers';
917
import { Predicates } from '../src/predicates';
1018
import { addCommonQueryTests } from './commonAdapterTests';
1119

@@ -41,7 +49,7 @@ describe('AsyncStorageAdapter tests', () => {
4149
describe('Query', () => {
4250
let Model: PersistentModelConstructor<Model>;
4351
let model1Id: string;
44-
const spyOnGetOne = jest.spyOn(ASAdapter, 'getById');
52+
const spyOnGetOne = jest.spyOn(ASAdapter, 'getByKey');
4553
const spyOnGetAll = jest.spyOn(ASAdapter, 'getAll');
4654
const spyOnMemory = jest.spyOn(ASAdapter, 'inMemoryPagination');
4755

@@ -92,9 +100,8 @@ describe('AsyncStorageAdapter tests', () => {
92100
await DataStore.clear();
93101
});
94102

95-
it('Should call getById for query by id', async () => {
103+
it('Should call getById for query by key', async () => {
96104
const result = await DataStore.query(Model, model1Id);
97-
98105
expect(result.field1).toEqual('Some value');
99106
expect(spyOnGetOne).toHaveBeenCalled();
100107
expect(spyOnGetAll).not.toHaveBeenCalled();
@@ -155,11 +162,16 @@ describe('AsyncStorageAdapter tests', () => {
155162
expect(spyOnMemory).not.toHaveBeenCalled();
156163
});
157164
});
165+
158166
describe('Delete', () => {
159167
let User: PersistentModelConstructor<User>;
160168
let Profile: PersistentModelConstructor<Profile>;
161169
let profile1Id: string;
162170
let user1Id: string;
171+
let Post: PersistentModelConstructor<Post>;
172+
let Comment: PersistentModelConstructor<Comment>;
173+
let post1Id: string;
174+
let comment1Id: string;
163175

164176
beforeAll(async () => {
165177
({ initSchema, DataStore } = require('../src/datastore/datastore'));
@@ -183,6 +195,25 @@ describe('AsyncStorageAdapter tests', () => {
183195
));
184196
});
185197

198+
beforeEach(async () => {
199+
const classes = initSchema(testSchema());
200+
201+
({ Post } = classes as {
202+
Post: PersistentModelConstructor<Post>;
203+
});
204+
205+
({ Comment } = classes as {
206+
Comment: PersistentModelConstructor<Comment>;
207+
});
208+
209+
const post = await DataStore.save(new Post({ title: 'Test' }));
210+
({ id: post1Id } = post);
211+
212+
({ id: comment1Id } = await DataStore.save(
213+
new Comment({ content: 'Test Content', post })
214+
));
215+
});
216+
186217
it('Should perform a cascading delete on a record with a Has One relationship', async () => {
187218
let user = await DataStore.query(User, user1Id);
188219
let profile = await DataStore.query(Profile, profile1Id);
@@ -197,8 +228,26 @@ describe('AsyncStorageAdapter tests', () => {
197228
profile = await DataStore.query(Profile, profile1Id);
198229

199230
// both should be undefined, even though we only explicitly deleted the user
200-
expect(user).toBeUndefined;
201-
expect(profile).toBeUndefined;
231+
expect(user).toBeUndefined();
232+
expect(profile).toBeUndefined();
233+
});
234+
235+
it('Should perform a cascading delete on a record with a Has Many relationship', async () => {
236+
let post = await DataStore.query(Post, post1Id);
237+
let comment = await DataStore.query(Comment, comment1Id);
238+
239+
// double-checking that both of the records exist at first
240+
expect(post.id).toEqual(post1Id);
241+
expect(comment.id).toEqual(comment1Id);
242+
243+
await DataStore.delete(Post, post.id);
244+
245+
post = await DataStore.query(Post, post1Id);
246+
comment = await DataStore.query(Comment, comment1Id);
247+
248+
// both should be undefined, even though we only explicitly deleted the post
249+
expect(post).toBeUndefined();
250+
expect(comment).toBeUndefined();
202251
});
203252
});
204253

0 commit comments

Comments
 (0)