Skip to content

Commit

Permalink
add scenario tests, 2 failed tests and 3 config tests to go
Browse files Browse the repository at this point in the history
  • Loading branch information
runspired committed Jul 23, 2019
1 parent 8e68df8 commit 5df6dae
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -281,12 +281,18 @@ if (IDENTIFIERS) {
assert.ok(updateMethodCalls === 1, 'We made a single call to our update method after save');
});

test(`We can configure the reset method`, async function(assert) {
/*
test(`The reset method is called when the application is destroyed`, async function(assert) {
assert.ok(false, 'not implemented');
});
test(`We can configure the forget method`, async function(assert) {
test(`The forget method is called when a record deletion is persisted`, async function(assert) {
assert.ok(false, 'not implemented');
});
test(`The forget method is called when a record unload results in full removal`, async function(assert) {
assert.ok(false, 'not implemented');
});
*/
});
}
322 changes: 308 additions & 14 deletions packages/-ember-data/tests/integration/identifiers/scenarios-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,54 @@ import Adapter from '@ember-data/adapter';
import Serializer from '@ember-data/serializer';
import { resolve } from 'rsvp';
import { ExistingResourceObject } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api';
import { Dict } from '@ember-data/store/-private/ts-interfaces/utils';
import { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier';
import { set } from '@ember/object';

function isNonEmptyString(str: any): str is string {
return typeof str === 'string' && str.length > 0;
}

if (IDENTIFIERS) {
module('Integration | Identifiers - scenarios', function(hooks) {
setupTest(hooks);

module('Secondary Cache based on an attribute', function() {
module('Secondary Cache based on an attribute', function(hooks) {
let store;
let calls;
let secondaryCache: {
id: Dict<string, string>;
username: Dict<string, string>;
};
class TestSerializer extends Serializer {
normalizeResponse(_, __, payload) {
return payload;
}
}
class TestAdapter extends Adapter {
shouldBackgroundReloadRecord() {
return false;
}
findRecord(isQuery = false) {
if (isQuery !== true) {
calls.findRecord++;
}
return resolve({
data: {
id: '1',
type: 'user',
attributes: {
firstName: 'Chris',
username: '@runspired',
age: 31,
},
},
});
}
queryRecord() {
calls.queryRecord++;
return this.findRecord(true);
}
}

hooks.beforeEach(function() {
const { owner } = this;
Expand All @@ -32,17 +71,64 @@ if (IDENTIFIERS) {
@attr() age: number;
}

owner.register('adapter:application', TestAdapter);
owner.register('serializer:application', TestSerializer);
owner.register('model:user', User);
owner.register('service:store', Store);

store = owner.lookup('service:store');
calls = {
findRecord: 0,
queryRecord: 0,
};

let localIdInc = 9000;
secondaryCache = {
id: Object.create(null),
username: Object.create(null),
};
const generationMethod = (resource: ExistingResourceObject) => {
if (typeof resource.type !== 'string' || resource.type.length < 1) {
throw new Error(`Cannot generate an lid for a record without a type`);
}

if (resource.type === 'user') {
let lid = resource.lid;
let username = resource.attributes && resource.attributes.username;

// try the username cache
if (!lid && isNonEmptyString(username)) {
lid = secondaryCache.username[username];
}

// try the id cache
if (!lid && isNonEmptyString(resource.id)) {
// if no entry, fallback to generation
lid = secondaryCache.id[resource.id] || `remote:user:${resource.id}`;
}

// generate from username if still undefined
if (!lid && isNonEmptyString(username)) {
lid = `remote:user:${username}`;
}

// generate at random if still undefined
if (!lid) {
lid = `local:user:${localIdInc++}`;
}

// sync all possible caches
if (isNonEmptyString(username)) {
secondaryCache.username[username] = lid;
}
if (isNonEmptyString(resource.id)) {
secondaryCache.id[resource.id] = lid;
}

return lid;
}

// handle non user cases
if (typeof resource.lid === 'string' && resource.lid.length > 0) {
return resource.lid;
}
Expand All @@ -64,23 +150,231 @@ if (IDENTIFIERS) {
setIdentifierForgetMethod(null);
});

test(`findRecord id then username with preload`, async function(assert) {
assert.ok(false, 'not implemented');
test(`findRecord id then queryRecord with username`, async function(assert) {
const recordById = await store.findRecord('user', '1');
const identifierById = recordIdentifierFor(recordById);
const recordByUsername = await store.queryRecord('user', { username: '@runspired' });
const identifierByUsername = recordIdentifierFor(recordByUsername);

assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical');
assert.strictEqual(recordById, recordByUsername, 'The records should be identical');
assert.strictEqual(calls.findRecord, 1, 'We made one call to Adapter.findRecord');
assert.strictEqual(calls.queryRecord, 1, 'We made one call to Adapter.queryRecord');
});
test(`queryRecord with username then findRecord with id`, async function(assert) {
const recordByUsername = await store.queryRecord('user', { username: '@runspired' });
const identifierByUsername = recordIdentifierFor(recordByUsername);
const recordById = await store.findRecord('user', '1');
const identifierById = recordIdentifierFor(recordById);

assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical');
assert.strictEqual(recordById, recordByUsername, 'The records should be identical');
assert.strictEqual(calls.findRecord, 0, 'We made zero calls to Adapter.findRecord');
assert.strictEqual(calls.queryRecord, 1, 'We made one call to Adapter.queryRecord');
});
test(`queryRecord with username and findRecord with id in parallel`, async function(assert) {
const recordByUsernamePromise1 = store.queryRecord('user', { username: '@runspired' });
const recordByIdPromise = store.findRecord('user', '1');
const recordByUsernamePromise2 = store.queryRecord('user', { username: '@runspired' });

const recordByUsername1 = await recordByUsernamePromise1;
const recordById = await recordByIdPromise;
const recordByUsername2 = await recordByUsernamePromise2;

const identifierById = recordIdentifierFor(recordById);
const identifierByUsername1 = recordIdentifierFor(recordByUsername1);
const identifierByUsername2 = recordIdentifierFor(recordByUsername2);

assert.strictEqual(identifierById, identifierByUsername1, 'The identifiers should be identical');
assert.strictEqual(identifierById, identifierByUsername2, 'The identifiers should be identical');
assert.strictEqual(recordById, recordByUsername1, 'The records should be identical');
assert.strictEqual(recordById, recordByUsername2, 'The records should be identical');
assert.strictEqual(calls.findRecord, 1, 'We made one call to Adapter.findRecord');
assert.strictEqual(calls.queryRecord, 2, 'We made two calls to Adapter.queryRecord');
});
test(`findRecord username with preload then id`, async function(assert) {
assert.ok(false, 'not implemented');
});

module('Secondary Cache using an attribute as an alternate id', function(hooks) {
let store;
let calls;
let secondaryCache: Dict<string, string>;
class TestSerializer extends Serializer {
normalizeResponse(_, __, payload) {
return payload;
}
}
class TestAdapter extends Adapter {
shouldBackgroundReloadRecord() {
return false;
}
findRecord(isQuery = false) {
if (isQuery !== true) {
calls.findRecord++;
}
return resolve({
data: {
id: '1',
type: 'user',
attributes: {
firstName: 'Chris',
username: '@runspired',
age: 31,
},
},
});
}
queryRecord() {
calls.queryRecord++;
return this.findRecord(true);
}
}

hooks.beforeEach(function() {
const { owner } = this;

class User extends Model {
@attr() firstName: string;
@attr() username: string;
@attr() age: number;
}

owner.register('adapter:application', TestAdapter);
owner.register('serializer:application', TestSerializer);
owner.register('model:user', User);
owner.register('service:store', Store);

store = owner.lookup('service:store');
calls = {
findRecord: 0,
queryRecord: 0,
};

let localIdInc = 9000;
secondaryCache = Object.create(null);

function lidForUser(resource) {
if (resource.type === 'user') {
let lid = resource.lid;
let username = resource.attributes && resource.attributes.username;

// try the username cache
if (!lid && isNonEmptyString(username)) {
lid = secondaryCache[username];
}

// try the id cache
if (!lid && isNonEmptyString(resource.id)) {
// first treat as id
// then treat as username
// if still no entry, fallback to generation
lid = secondaryCache[resource.id] || `remote:user:${resource.id}`;
}

// generate from username if still undefined
if (!lid && isNonEmptyString(username)) {
lid = `remote:user:${username}`;
}

// generate at random if still undefined
if (!lid) {
lid = `local:user:${localIdInc++}`;
}

// sync all possible caches
if (isNonEmptyString(username)) {
secondaryCache[username] = lid;
}
if (isNonEmptyString(resource.id)) {
secondaryCache[resource.id] = lid;
}

return lid;
}
}

const generationMethod = (resource: ExistingResourceObject) => {
if (typeof resource.type !== 'string' || resource.type.length < 1) {
throw new Error(`Cannot generate an lid for a record without a type`);
}

if (resource.type === 'user') {
return lidForUser(resource);
}

// handle non user cases
if (typeof resource.lid === 'string' && resource.lid.length > 0) {
return resource.lid;
}

if (typeof resource.id === 'string' && resource.id.length > 0) {
return `remote:${resource.type}:${resource.id}`;
}

return `local:${resource.type}:${localIdInc++}`;
};

const updateMethod = (identifier: StableRecordIdentifier, resource: ExistingResourceObject) => {
resource.lid = identifier.lid;
lidForUser(resource);
};

setIdentifierGenerationMethod(generationMethod);
setIdentifierUpdateMethod(updateMethod);
});
test(`findRecord id then username as id`, async function(assert) {
assert.ok(false, 'not implemented');

hooks.afterEach(function() {
setIdentifierGenerationMethod(null);
setIdentifierResetMethod(null);
setIdentifierUpdateMethod(null);
setIdentifierForgetMethod(null);
});
test(`findRecord username as id then id`, async function(assert) {
assert.ok(false, 'not implemented');

test(`findRecord by id then by username as id`, async function(assert) {
const recordById = await store.findRecord('user', '1');
const identifierById = recordIdentifierFor(recordById);
const recordByUsername = await store.findRecord('user', '@runspired');
const identifierByUsername = recordIdentifierFor(recordByUsername);

assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical');
assert.strictEqual(recordById, recordByUsername, 'The records should be identical');
assert.strictEqual(calls.findRecord, 1, 'We made only one call to Adapter.findRecord');
assert.strictEqual(recordById.id, '1', 'The record id is correct');
assert.strictEqual(identifierById.id, '1', 'The identifier id is correct');
});
test(`push id then username`, async function(assert) {
assert.ok(false, 'not implemented');

test(`findRecord by username as id then by id`, async function(assert) {
const recordByUsername = await store.findRecord('user', '@runspired');
const identifierByUsername = recordIdentifierFor(recordByUsername);
const recordById = await store.findRecord('user', '1');
const identifierById = recordIdentifierFor(recordById);

assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical');
assert.strictEqual(recordById, recordByUsername, 'The records should be identical');
assert.strictEqual(calls.findRecord, 1, 'We made one call to Adapter.findRecord');
assert.strictEqual(recordById.id, '1', 'The record id is correct');
assert.strictEqual(identifierById.id, '1', 'The identifier id is correct');
});
test(`push username then id`, async function(assert) {
assert.ok(false, 'not implemented');

test(`push id then findRecord username`, async function(assert) {
const recordById = store.push({
data: {
type: 'user',
id: '1',
attributes: {
username: '@runspired',
firstName: 'Chris',
age: 31,
},
},
});
const identifierById = recordIdentifierFor(recordById);
const recordByUsername = await store.findRecord('user', '@runspired');
const identifierByUsername = recordIdentifierFor(recordByUsername);

assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical');
assert.strictEqual(recordById, recordByUsername, 'The records should be identical');
assert.strictEqual(recordById.id, '1', 'The record id is correct');
assert.strictEqual(identifierById.id, '1', 'The identifier id is correct');
});
});
});
Expand Down
1 change: 0 additions & 1 deletion packages/store/addon/-private/identifiers/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,6 @@ function performRecordIdentifierUpdate(
let newId = coerceId(id);

if (identifier.id !== null && identifier.id !== newId) {
debugger;
throw new Error(
`The 'id' for a RecordIdentifier cannot be updated once it has been set. Attempted to set id for '${wrapper}' to '${newId}'.`
);
Expand Down
Loading

0 comments on commit 5df6dae

Please sign in to comment.