Skip to content

Commit

Permalink
fix(datastore): cascade deleted for nested Has Many
Browse files Browse the repository at this point in the history
  • Loading branch information
iartemiev committed Jan 19, 2023
1 parent dd06603 commit 0ec5a60
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 11 deletions.
34 changes: 33 additions & 1 deletion packages/datastore/__tests__/commonAdapterTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import {
Model,
User,
Profile,
Blog,
Post,
Comment,
testSchema,
CompositePKParent,
HasOneParent,
HasOneChild,
MtmLeft,
Expand Down Expand Up @@ -377,6 +377,7 @@ export function addCommonQueryTests({

describe('common `delete()` cases', () => {
let Comment: PersistentModelConstructor<Comment>;
let Blog: PersistentModelConstructor<Blog>;
let Post: PersistentModelConstructor<Post>;
let HasOneParent: PersistentModelConstructor<HasOneParent>;
let HasOneChild: PersistentModelConstructor<HasOneChild>;
Expand All @@ -387,13 +388,15 @@ export function addCommonQueryTests({
const classes = initSchema(testSchema());
({
Comment,
Blog,
Post,
HasOneParent,
HasOneChild,
DefaultPKHasOneParent,
DefaultPKHasOneChild,
} = classes as {
Comment: PersistentModelConstructor<Comment>;
Blog: PersistentModelConstructor<Blog>;
Post: PersistentModelConstructor<Post>;
HasOneParent: PersistentModelConstructor<HasOneParent>;
HasOneChild: PersistentModelConstructor<HasOneChild>;
Expand Down Expand Up @@ -503,6 +506,35 @@ export function addCommonQueryTests({
}
);

(isSQLiteAdapter() ? test.skip : test)(
'deleting nested hasMany cascades',
async () => {
const blog = await DataStore.save(
new Blog({
title: 'my blog',
})
);

const post = await DataStore.save(
new Post({
title: 'my post',
blogId: blog.id,
})
);

const retrievedBlog = await DataStore.query(Blog, blog.id);
expect(retrievedBlog!.id).toEqual(blog.id);

const posts = await retrievedBlog!.posts.toArray();
expect(posts.length).toEqual(1);
expect(posts[0].id).toEqual(post.id);

await DataStore.delete(Blog, blog.id);

expect(await DataStore.query(Post, post.id)).toBeUndefined();
}
);

test('deleting belongsTo side of relationship does not cascade', async () => {
const post = await DataStore.save(
new Post({
Expand Down
83 changes: 80 additions & 3 deletions packages/datastore/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import Observable, { ZenObservable } from 'zen-observable-ts';
import { parse } from 'graphql';
import { ModelInit, Schema, InternalSchema, __modelMeta__ } from '../src/types';
import {
ModelInit,
Schema,
InternalSchema,
isModelAttributePrimaryKey,
__modelMeta__,
} from '../src/types';
import {
AsyncCollection,
MutableModel,
Expand Down Expand Up @@ -530,8 +536,7 @@ class FakeGraphQLService {
this.tables.set(model.name, new Map<string, any[]>());
let CPKFound = false;
for (const attribute of model.attributes || []) {
// Pretty sure the first key is the PK.
if (attribute.type === 'key') {
if (isModelAttributePrimaryKey(attribute)) {
this.PKFields.set(model.name, attribute!.properties!.fields);
CPKFound = true;
break;
Expand Down Expand Up @@ -985,6 +990,7 @@ export function getDataStore({ online = false, isNode = true } = {}) {
const classes = initSchema(testSchema());
const {
ModelWithBoolean,
Blog,
Post,
Comment,
User,
Expand All @@ -1004,6 +1010,7 @@ export function getDataStore({ online = false, isNode = true } = {}) {
DefaultPKHasOneChild,
} = classes as {
ModelWithBoolean: PersistentModelConstructor<ModelWithBoolean>;
Blog: PersistentModelConstructor<Blog>;
Post: PersistentModelConstructor<Post>;
Comment: PersistentModelConstructor<Comment>;
User: PersistentModelConstructor<User>;
Expand All @@ -1030,6 +1037,7 @@ export function getDataStore({ online = false, isNode = true } = {}) {
simulateConnect,
simulateDisconnect,
ModelWithBoolean,
Blog,
Post,
Comment,
User,
Expand Down Expand Up @@ -1109,10 +1117,24 @@ export declare class Login {
constructor(init: Login);
}

export declare class Blog {
public readonly id: string;
public readonly title: string;
public readonly posts: AsyncCollection<Post>;

constructor(init: ModelInit<Blog>);

static copyOf(
src: Blog,
mutator: (draft: MutableModel<Blog>) => void | Blog
): Blog;
}

export declare class Post {
public readonly id: string;
public readonly title: string;
public readonly comments: AsyncCollection<Comment>;
public readonly blogId?: string;

constructor(init: ModelInit<Post>);

Expand Down Expand Up @@ -1720,6 +1742,47 @@ export function testSchema(): Schema {
},
},
},
Blog: {
name: 'Blog',
fields: {
id: {
name: 'id',
isArray: false,
type: 'ID',
isRequired: true,
attributes: [],
},
title: {
name: 'title',
isArray: false,
type: 'String',
isRequired: true,
attributes: [],
},
posts: {
name: 'posts',
isArray: true,
type: {
model: 'Post',
},
isRequired: false,
attributes: [],
isArrayNullable: true,
association: {
connectionType: 'HAS_MANY',
associatedWith: ['blogId'],
},
},
},
syncable: true,
pluralName: 'Blogs',
attributes: [
{
type: 'model',
properties: {},
},
],
},
Post: {
name: 'Post',
fields: {
Expand All @@ -1737,6 +1800,13 @@ export function testSchema(): Schema {
isRequired: true,
attributes: [],
},
blogId: {
name: 'blogId',
isArray: false,
type: 'ID',
isRequired: false,
attributes: [],
},
comments: {
name: 'comments',
isArray: true,
Expand All @@ -1759,6 +1829,13 @@ export function testSchema(): Schema {
type: 'model',
properties: {},
},
{
type: 'key',
properties: {
name: 'byBlog',
fields: ['blogId'],
},
},
],
},
Comment: {
Expand Down
22 changes: 18 additions & 4 deletions packages/datastore/src/storage/adapter/AsyncStorageAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,14 +600,20 @@ export class AsyncStorageAdapter implements Adapter {

const allRecords = await this.db.getAll(storeName);

const recordToDelete = allRecords.filter(
const recordsToDelete = allRecords.filter(
childItem => childItem[hasOneIndex as string] === value
) as T[];

// instantiate models before passing to deleteTraverse
// necessary for extracting PK values via getIndexKeyValuesFromModel
const modelsToDelete = recordsToDelete.length
? await this.load(nameSpace, modelName, recordsToDelete)
: [];

await this.deleteTraverse<T>(
this.schema.namespaces[nameSpace].relationships![modelName]
.relationTypes,
recordToDelete,
modelsToDelete,
modelName,
nameSpace,
deleteQueue
Expand All @@ -624,14 +630,22 @@ export class AsyncStorageAdapter implements Adapter {

const indices = index!.split(IDENTIFIER_KEY_SEPARATOR);

const childrenArray = allRecords.filter(childItem =>
const childRecords = allRecords.filter(childItem =>
indices.every(index => keyValues.includes(childItem[index]))
) as T[];

// instantiate models before passing to deleteTraverse
// necessary for extracting PK values via getIndexKeyValuesFromModel
const childModels = await this.load(
nameSpace,
modelName,
childRecords
);

await this.deleteTraverse<T>(
this.schema.namespaces[nameSpace].relationships![modelName]
.relationTypes,
childrenArray,
childModels,
modelName,
nameSpace,
deleteQueue
Expand Down
20 changes: 17 additions & 3 deletions packages/datastore/src/storage/adapter/IndexedDBAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -912,10 +912,16 @@ class IndexedDBAdapter implements Adapter {
.get(this.canonicalKeyPath(values))
);

// instantiate models before passing to deleteTraverse
// necessary for extracting PK values via getIndexKeyValuesFromModel
const modelsToDelete = recordToDelete
? await this.load(nameSpace, modelName, [recordToDelete])
: [];

await this.deleteTraverse(
this.schema.namespaces[nameSpace].relationships![modelName]
.relationTypes,
recordToDelete ? [recordToDelete] : [],
modelsToDelete,
modelName,
nameSpace,
deleteQueue
Expand All @@ -940,16 +946,24 @@ class IndexedDBAdapter implements Adapter {
);
const keyValues = this.getIndexKeyValuesFromModel(model);

const childrenArray = await this.db
const childRecords = await this.db
.transaction(storeName, 'readwrite')
.objectStore(storeName)
.index(index as string)
.getAll(this.canonicalKeyPath(keyValues));

// instantiate models before passing to deleteTraverse
// necessary for extracting PK values via getIndexKeyValuesFromModel
const childModels = await this.load(
nameSpace,
modelName,
childRecords
);

await this.deleteTraverse(
this.schema.namespaces[nameSpace].relationships![modelName]
.relationTypes,
childrenArray,
childModels,
modelName,
nameSpace,
deleteQueue
Expand Down

0 comments on commit 0ec5a60

Please sign in to comment.