Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add warning message when trying to use SQLite with CPK enabled #11027

Merged
merged 13 commits into from
Mar 17, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* SQLiteCPKDisabled.test.ts and SQLiteCPKEnabled.test.ts exist in two separate
* files in order to test DataStore with two separate schemas.
* jest.isolateModules was not correctly isolating the instances of DataStore
* resulting in the DataStore singleton being shared between the tests.
* The schema can only be initialized once in DataStore, making it impossible to
* test the effect of different schemas.
*
* Both files should be removed when CPK support is added.
*/
import sqlite3 from 'sqlite3';
sqlite3.verbose();
import Observable from 'zen-observable';
import SQLiteAdapter from '../src/SQLiteAdapter/SQLiteAdapter';
import { testSchema } from './helpers';
import { initSchema, DataStore } from '@aws-amplify/datastore';

jest.mock('@aws-amplify/datastore/src/sync/datastoreConnectivity', () => {
return {
status: () => Observable.of(false) as any,
unsubscribe: () => {},
socketDisconnected: () => {},
};
});

jest.mock('react-native-sqlite-storage', () => {
return {
async openDatabase(name, version, displayname, size) {
return new InnerSQLiteDatabase();
},
async deleteDatabase(name) {},
enablePromise(enabled) {},
DEBUG(debug) {},
};
});

const sqlog = [];

/**
* A lower-level SQLite wrapper to test SQLiteAdapter against.
* It's intended to be fast, using an in-memory database.
*/
class InnerSQLiteDatabase {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If fighting with jest is more effort than it's worth, can this at least be moved to a util file? Is there a different between these database fakes?

private innerDB;

constructor() {
this.innerDB = new sqlite3.Database(':memory:');
}

async executeSql(
statement,
params: any[] = [],
callback: ((...args) => Promise<any>) | undefined = undefined,
logger = undefined
) {
sqlog.push(`${statement}; ${JSON.stringify(params)}`);
if (statement.trim().toLowerCase().startsWith('select')) {
return new Promise(async resolve => {
const rows: any[] = [];
const resultSet = {
rows: {
get length() {
return rows.length;
},
raw: () => rows,
},
};

await this.innerDB.each(
statement,
params,
async (err, row) => {
if (err) {
console.error('SQLite ERROR', new Error(err));
console.warn(statement, params);
}
rows.push(row);
},
() => {
resolve([resultSet]);
}
);

if (typeof callback === 'function') await callback(this, resultSet);
});
} else {
return await this.innerDB.run(statement, params, err => {
if (typeof callback === 'function') {
callback(err);
} else if (err) {
console.error('calback', err);
throw err;
}
});
}
}

async transaction(fn) {
return this.innerDB.serialize(await fn(this));
}

async readTransaction(fn) {
return this.innerDB.serialize(await fn(this));
}

async close() {}
}

describe('SQLite CPK Disabled', () => {
test('does not log error when schema is generated with targetName (cpk disabled)', async () => {
console.error = jest.fn();
const { initSchema, DataStore } = require('@aws-amplify/datastore');
DataStore.configure({
storageAdapter: SQLiteAdapter,
});
initSchema(testSchema());
await DataStore.start();
expect(console.error as any).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* SQLiteCPKDisabled.test.ts and SQLiteCPKEnabled.test.ts exist in two separate
* files in order to test DataStore with two separate schemas.
* jest.isolateModules was not correctly isolating the instances of DataStore
* resulting in the DataStore singleton being shared between the tests.
* The schema can only be initialized once in DataStore, making it impossible to
* test the effect of different schemas.
*
* Both files should be removed when CPK support is added.
*/
import sqlite3 from 'sqlite3';
sqlite3.verbose();
import Observable from 'zen-observable';
import SQLiteAdapter from '../src/SQLiteAdapter/SQLiteAdapter';
import { testSchema } from './helpers';
import { initSchema, DataStore } from '@aws-amplify/datastore';

jest.mock('@aws-amplify/datastore/src/sync/datastoreConnectivity', () => {
return {
status: () => Observable.of(false) as any,
unsubscribe: () => {},
socketDisconnected: () => {},
};
});

jest.mock('react-native-sqlite-storage', () => {
return {
async openDatabase(name, version, displayname, size) {
return new InnerSQLiteDatabase();
},
async deleteDatabase(name) {},
enablePromise(enabled) {},
DEBUG(debug) {},
};
});

const sqlog = [];

/**
* A lower-level SQLite wrapper to test SQLiteAdapter against.
* It's intended to be fast, using an in-memory database.
*/
class InnerSQLiteDatabase {
private innerDB;

constructor() {
this.innerDB = new sqlite3.Database(':memory:');
}

async executeSql(
statement,
params: any[] = [],
callback: ((...args) => Promise<any>) | undefined = undefined,
logger = undefined
) {
sqlog.push(`${statement}; ${JSON.stringify(params)}`);
if (statement.trim().toLowerCase().startsWith('select')) {
return new Promise(async resolve => {
const rows: any[] = [];
const resultSet = {
rows: {
get length() {
return rows.length;
},
raw: () => rows,
},
};

await this.innerDB.each(
statement,
params,
async (err, row) => {
if (err) {
console.error('SQLite ERROR', new Error(err));
console.warn(statement, params);
}
rows.push(row);
},
() => {
resolve([resultSet]);
}
);

if (typeof callback === 'function') await callback(this, resultSet);
});
} else {
return await this.innerDB.run(statement, params, err => {
if (typeof callback === 'function') {
callback(err);
} else if (err) {
console.error('calback', err);
throw err;
}
});
}
}

async transaction(fn) {
return this.innerDB.serialize(await fn(this));
}

async readTransaction(fn) {
return this.innerDB.serialize(await fn(this));
}

async close() {}
}

describe('SQLite SPK Enabled', () => {
test('logs error when schema is generated with targetNames (cpk enabled)', async () => {
console.error = jest.fn();
const { initSchema, DataStore } = require('@aws-amplify/datastore');
DataStore.configure({
storageAdapter: SQLiteAdapter,
});
console.log(JSON.stringify(testSchema(), null, 2));
initSchema({
...testSchema(),
models: {
Post: {
name: 'Post',
fields: {
id: {
name: 'id',
isArray: false,
type: 'ID',
isRequired: true,
attributes: [],
},
title: {
name: 'title',
isArray: false,
type: 'String',
isRequired: true,
attributes: [],
},
comments: {
name: 'comments',
isArray: true,
type: {
model: 'Comment',
},
isRequired: true,
attributes: [],
isArrayNullable: true,
association: {
connectionType: 'HAS_MANY',
associatedWith: 'postId',
},
},
},
syncable: true,
pluralName: 'Posts',
attributes: [
{
type: 'model',
properties: {},
},
],
},
Comment: {
name: 'Comment',
fields: {
id: {
name: 'id',
isArray: false,
type: 'ID',
isRequired: true,
attributes: [],
},
content: {
name: 'content',
isArray: false,
type: 'String',
isRequired: true,
attributes: [],
},
post: {
name: 'post',
isArray: false,
type: {
model: 'Post',
},
isRequired: false,
attributes: [],
association: {
connectionType: 'BELONGS_TO',
targetNames: ['postId'],
},
},
},
syncable: true,
pluralName: 'Comments',
attributes: [
{
type: 'model',
properties: {},
},
{
type: 'key',
properties: {
name: 'byPost',
fields: ['postId'],
},
},
],
},
},
});
await DataStore.start();
expect((console.error as any).mock.calls[0][0]).toMatch(
'The SQLite adapter does not support schemas using custom primary key. Set `graphQLTransformer.respectPrimaryKeyAttributesOnConnectionField in amplify/cli.json to false to disable custom primary key. Then, to properly regenerate your API, add an empty newline to your GraphQL schema and run `amplify push`.'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ export class CommonSQLiteAdapter implements StorageAdapter {
this.getModelConstructorByModelName = getModelConstructorByModelName;

try {
const usesCPKCodegen = Object.values(
this.schema.namespaces.user.models
).some(model =>
Object.values(model.fields).some(field =>
field.association?.hasOwnProperty('targetNames')
)
);
if (usesCPKCodegen) {
logger.error(
'The SQLite adapter does not support schemas using custom primary key. Set `graphQLTransformer.respectPrimaryKeyAttributesOnConnectionField in `amplify/cli.json` to false to disable custom primary key. Then, to properly regenerate your API, add an empty newline to your GraphQL schema and run `amplify push`.'
dpilch marked this conversation as resolved.
Show resolved Hide resolved
);
}
await this.db.init();
const statements = generateSchemaStatements(this.schema);
await this.db.createSchema(statements);
Expand Down