Skip to content

Commit

Permalink
feat(@aws-amplify/datastore): Support non-@model types in DataStore (#…
Browse files Browse the repository at this point in the history
…5128)

* Upgrade immer

* Return sooner if predicates are empty

* Fix bug when trying to delete a model instance that is not persisted

* Support non-@model types

* Generate datastore coverage report and RN integ test

* Fix tslint error

* Remove  unit test case for onGetPost

* Remove unused code

* Rename instance initializer to initializeInstance

* Rename SchemaType to SchemaNonModel

* Rename types to nonModels in schema.js

* Rename type to nonModel

* Make nonModels optional in schema.js

* Remove generic constraint from createTypeClass

* Rename ModelOrTypeConstructorMap to TypeConstructorMap

* Rename createModelAndTypeClassses to createTypeClasses

* Rename createTypeClass to createNonModelClass
  • Loading branch information
manueliglesias authored Mar 19, 2020
1 parent 0ddb6af commit b884ea2
Show file tree
Hide file tree
Showing 17 changed files with 726 additions and 175 deletions.
5 changes: 5 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
yarn run test --scope @aws-amplify/analytics
yarn run test --scope @aws-amplify/cache
yarn run test --scope @aws-amplify/core
yarn run test --scope @aws-amplify/datastore
yarn run test --scope @aws-amplify/interactions
yarn run test --scope @aws-amplify/pubsub
yarn run test --scope @aws-amplify/predictions
Expand Down Expand Up @@ -312,6 +313,10 @@ jobs:
cp -rf ~/amplify-js/packages/core/lib ./node_modules/@aws-amplify/core/lib
cp -rf ~/amplify-js/packages/core/lib-esm ./node_modules/@aws-amplify/core/lib-esm
cp -rf ~/amplify-js/packages/datastore/dist ./node_modules/@aws-amplify/datastore/dist
cp -rf ~/amplify-js/packages/datastore/lib ./node_modules/@aws-amplify/datastore/lib
cp -rf ~/amplify-js/packages/datastore/lib-esm ./node_modules/@aws-amplify/datastore/lib-esm
cp -rf ~/amplify-js/packages/interactions/dist ./node_modules/@aws-amplify/interactions/dist
cp -rf ~/amplify-js/packages/interactions/lib ./node_modules/@aws-amplify/interactions/lib
cp -rf ~/amplify-js/packages/interactions/lib-esm ./node_modules/@aws-amplify/interactions/lib-esm
Expand Down
114 changes: 103 additions & 11 deletions packages/datastore/__tests__/DataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
MutableModel,
PersistentModelConstructor,
Schema,
NonModelTypeConstructor,
} from '../src/types';
import StorageType from '../src/storage/storage';
import Observable from 'zen-observable-ts';
Expand Down Expand Up @@ -36,20 +37,20 @@ beforeEach(() => {

describe('DataStore tests', () => {
describe('initSchema tests', () => {
test('Class is created', () => {
test('Model class is created', () => {
const classes = initSchema(testSchema());

expect(classes).toHaveProperty('Model');

const { Model } = classes;
const { Model } = classes as { Model: PersistentModelConstructor<Model> };

let property: keyof PersistentModelConstructor<any> = 'copyOf';
expect(Model).toHaveProperty(property);

expect(typeof Model.copyOf).toBe('function');
});

test('Class can be instantiated', () => {
test('Model class can be instantiated', () => {
const { Model } = initSchema(testSchema()) as {
Model: PersistentModelConstructor<Model>;
};
Expand Down Expand Up @@ -92,6 +93,32 @@ describe('DataStore tests', () => {
initSchema(testSchema());
}).toThrow('The schema has already been initialized');
});

test('Non @model class is created', () => {
const classes = initSchema(testSchema());

expect(classes).toHaveProperty('Metadata');

const { Metadata } = classes;

let property: keyof PersistentModelConstructor<any> = 'copyOf';
expect(Metadata).not.toHaveProperty(property);
});

test('Non @model class can be instantiated', () => {
const { Metadata } = initSchema(testSchema()) as {
Metadata: NonModelTypeConstructor<Metadata>;
};

const metadata = new Metadata({
author: 'some author',
tags: [],
});

expect(metadata).toBeInstanceOf(Metadata);

expect(metadata).not.toHaveProperty('id');
});
});

describe('Immutability', () => {
Expand Down Expand Up @@ -147,6 +174,20 @@ describe('DataStore tests', () => {
// ID should be kept the same
expect(model1.id).toBe(model2.id);
});

test('Non @model - Field cannot be changed', () => {
const { Metadata } = initSchema(testSchema()) as {
Metadata: NonModelTypeConstructor<Metadata>;
};

const nonModel = new Metadata({
author: 'something',
});

expect(() => {
(<any>nonModel).author = 'edit';
}).toThrowError("Cannot assign to read only property 'author' of object");
});
});

describe('Initialization', () => {
Expand All @@ -155,7 +196,7 @@ describe('DataStore tests', () => {

const classes = initSchema(testSchema());

const { Model } = classes;
const { Model } = classes as { Model: PersistentModelConstructor<Model> };

const promises = [
DataStore.query(Model),
Expand All @@ -168,18 +209,33 @@ describe('DataStore tests', () => {

expect(Storage).toHaveBeenCalledTimes(1);
});
});

test('It is initialized when observing (no query)', async () => {
Storage = require('../src/storage/storage').default;
test('It is initialized when observing (no query)', async () => {
Storage = require('../src/storage/storage').default;

const classes = initSchema(testSchema());
const classes = initSchema(testSchema());

const { Model } = classes;
const { Model } = classes as { Model: PersistentModelConstructor<Model> };

DataStore.observe(Model).subscribe(jest.fn());
DataStore.observe(Model).subscribe(jest.fn());

expect(Storage).toHaveBeenCalledTimes(1);
expect(Storage).toHaveBeenCalledTimes(1);
});
});

test("non-@models can't be saved", async () => {
const { Metadata } = initSchema(testSchema()) as {
Metadata: NonModelTypeConstructor<Metadata>;
};

const metadata = new Metadata({
author: 'some author',
tags: [],
});

await expect(DataStore.save(<any>metadata)).rejects.toThrow(
'Object is not an instance of a valid model'
);
});
});

Expand All @@ -197,6 +253,12 @@ declare class Model {
): Model;
}

export declare class Metadata {
readonly author: string;
readonly tags?: string[];
constructor(init: Metadata);
}

function testSchema(): Schema {
return {
enums: {},
Expand All @@ -218,6 +280,15 @@ function testSchema(): Schema {
type: 'String',
isRequired: true,
},
metadata: {
name: 'metadata',
isArray: false,
type: {
nonModel: 'Metadata',
},
isRequired: false,
attributes: [],
},
},
},
LocalModel: {
Expand All @@ -240,6 +311,27 @@ function testSchema(): Schema {
},
},
},
nonModels: {
Metadata: {
name: 'Metadata',
fields: {
author: {
name: 'author',
isArray: false,
type: 'String',
isRequired: true,
attributes: [],
},
tags: {
name: 'tags',
isArray: true,
type: 'String',
isRequired: false,
attributes: [],
},
},
},
},
version: '1',
};
}
Expand Down
171 changes: 171 additions & 0 deletions packages/datastore/__tests__/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { parse, print } from 'graphql';
import { SchemaNamespace } from '../src';
import {
buildGraphQLOperation,
buildSubscriptionGraphQLOperation,
TransformerMutationType,
} from '../src/sync/utils';
import { newSchema } from './schema';

const postSelectionSet = `
id
title
metadata {
rating
tags
nested {
aField
}
}
_version
_lastChangedAt
_deleted
reference {
id
_deleted
}
blog {
id
_deleted
}
`;

describe('DataStore GraphQL generation', () => {
test.each([
[
'LIST',
/* GraphQL */ `
query operation(
$limit: Int
$nextToken: String
$lastSync: AWSTimestamp
) {
syncPosts(limit: $limit, nextToken: $nextToken, lastSync: $lastSync) {
items {
${postSelectionSet}
}
nextToken
startedAt
}
}
`,
],
[
'CREATE',
/* GraphQL */ `
mutation operation($input: CreatePostInput!) {
createPost(input: $input) {
${postSelectionSet}
}
}
`,
],
[
'UPDATE',
/* GraphQL */ `
mutation operation(
$input: UpdatePostInput!
$condition: ModelPostConditionInput
) {
updatePost(input: $input, condition: $condition) {
${postSelectionSet}
}
}
`,
],
,
[
'DELETE',
/* GraphQL */ `
mutation operation(
$input: DeletePostInput!
$condition: ModelPostConditionInput
) {
deletePost(input: $input, condition: $condition) {
${postSelectionSet}
}
}
`,
],
,
[
'GET',
/* GraphQL */ `
query operation($id: ID!) {
getPost(id: $id) {
${postSelectionSet}
}
}
`,
],
])(
'%s - has full selection set including types, and inputs',
(graphQLOpType, expectedGraphQL) => {
const namespace = <SchemaNamespace>(<unknown>newSchema);

const {
models: { Post: postModelDefinition },
} = namespace;

const [[, , query]] = buildGraphQLOperation(
namespace,
postModelDefinition,
<any>graphQLOpType
);

expect(print(parse(query))).toStrictEqual(print(parse(expectedGraphQL)));
}
);

test.each([
[
TransformerMutationType.CREATE,
/* GraphQL */ `
subscription operation {
onCreatePost {
${postSelectionSet}
}
}
`,
],
[
TransformerMutationType.UPDATE,
/* GraphQL */ `
subscription operation {
onUpdatePost {
${postSelectionSet}
}
}
`,
],
[
TransformerMutationType.DELETE,
/* GraphQL */ `
subscription operation {
onDeletePost {
${postSelectionSet}
}
}
`,
],
])(
'Subscription (%s) - has full selection set including types, and inputs',
(transformerMutationType, expectedGraphQL) => {
const namespace = <SchemaNamespace>(<unknown>newSchema);

const {
models: { Post: postModelDefinition },
} = namespace;

const [, , query] = buildSubscriptionGraphQLOperation(
namespace,
postModelDefinition,
<any>transformerMutationType,
false,
''
);

expect(print(parse(query))).toStrictEqual(print(parse(expectedGraphQL)));
}
);
});
Loading

0 comments on commit b884ea2

Please sign in to comment.