forked from emberjs/data
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat (private): implement support for derivations (emberjs#8939)
- Loading branch information
1 parent
89f3e9c
commit eaf7e2a
Showing
7 changed files
with
208 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type Store from '@ember-data/store'; | ||
import type { StableRecordIdentifier } from "@ember-data/types/q/identifier"; | ||
import { Destroy, SchemaRecord } from './record'; | ||
|
||
export function instantiateRecord(store: Store, identifier: StableRecordIdentifier, createArgs?: Record<string, unknown>): SchemaRecord { | ||
if (createArgs) { | ||
const editable = new SchemaRecord(store, identifier, true); | ||
Object.assign(editable, createArgs); | ||
return editable; | ||
} | ||
|
||
return new SchemaRecord(store, identifier, false); | ||
} | ||
|
||
export function teardownRecord(record: SchemaRecord): void { | ||
record[Destroy](); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import type Store from '@ember-data/store'; | ||
import type { StableRecordIdentifier } from "@ember-data/types/q/identifier"; | ||
import type { FieldSchema, SchemaService } from './schema'; | ||
import { Cache } from '@ember-data/types/q/cache'; | ||
|
||
export const Destroy = Symbol('Destroy'); | ||
export const RecordStore = Symbol('Store'); | ||
export const Identifier = Symbol('Identifier'); | ||
export const Editable = Symbol('Editable'); | ||
|
||
function computeAttribute(schema: SchemaService, cache: Cache, record: SchemaRecord, identifier: StableRecordIdentifier, field: FieldSchema, prop: string): unknown { | ||
const rawValue = cache.getAttr(identifier, prop); | ||
if (field.type === null) { | ||
return rawValue; | ||
} | ||
const transform = schema.transforms.get(field.type); | ||
if (!transform) { | ||
throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`); | ||
} | ||
return transform.hydrate(rawValue, field.options ?? null, record); | ||
} | ||
|
||
function computeDerivation(schema: SchemaService, record: SchemaRecord, identifier: StableRecordIdentifier, field: FieldSchema, prop: string): unknown { | ||
if (field.type === null) { | ||
throw new Error(`The schema for ${identifier.type}.${String(prop)} is missing the type of the derivation`); | ||
} | ||
|
||
const derivation = schema.derivations.get(field.type); | ||
if (!derivation) { | ||
throw new Error(`No '${field.type}' derivation defined for use by ${identifier.type}.${String(prop)}`); | ||
} | ||
return derivation(record, field.options ?? null, prop); | ||
} | ||
|
||
export class SchemaRecord { | ||
declare [RecordStore]: Store; | ||
declare [Identifier]: StableRecordIdentifier; | ||
declare [Editable]: boolean; | ||
|
||
constructor(store: Store, identifier: StableRecordIdentifier, editable: boolean) { | ||
this[RecordStore] = store; | ||
this[Identifier] = identifier; | ||
this[Editable] = editable; | ||
|
||
const schema = store.schema as unknown as SchemaService; | ||
const cache = store.cache; | ||
const fields = schema.fields(identifier); | ||
|
||
return new Proxy(this, { | ||
get(target, prop, receiver) { | ||
if (prop === Destroy) { | ||
return target[Destroy]; | ||
} | ||
|
||
if (prop === 'id') { | ||
return identifier.id; | ||
} | ||
if (prop === '$type') { | ||
return identifier.type; | ||
} | ||
const field = fields.get(prop as string); | ||
if (!field) { | ||
throw new Error(`No field named ${String(prop)} on ${identifier.type}`); | ||
} | ||
|
||
switch (field.kind) { | ||
case 'attribute': | ||
return computeAttribute(schema, cache, target, identifier, field, prop as string); | ||
case 'derived': | ||
return computeDerivation(schema, receiver, identifier, field, prop as string); | ||
default: | ||
throw new Error(`Field '${String(prop)}' on '${identifier.type}' has the unknown kind '${field.kind}'`); | ||
} | ||
|
||
}, | ||
set(target, prop, value) { | ||
if (!target[Editable]) { | ||
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because the record is not editable`); | ||
} | ||
|
||
const field = fields.get(prop as string); | ||
if (!field) { | ||
throw new Error(`There is no field named ${String(prop)} on ${identifier.type}`); | ||
} | ||
|
||
if (field.kind === 'attribute') { | ||
if (field.type === null) { | ||
cache.setAttr(identifier, prop as string, value); | ||
return true; | ||
} | ||
const transform = schema.transforms.get(field.type); | ||
|
||
if (!transform) { | ||
throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`); | ||
} | ||
|
||
const rawValue = transform.serialize(value, field.options ?? null, target); | ||
cache.setAttr(identifier, prop as string, rawValue); | ||
return true; | ||
} else if (field.kind === 'derived') { | ||
throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because it is derived`); | ||
} | ||
|
||
throw new Error(`Unknown field kind ${field.kind}`); | ||
}, | ||
}); | ||
} | ||
|
||
[Destroy](): void {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { SchemaRecord } from '@warp-drive/schema-record/record'; | ||
import { SchemaService } from '@warp-drive/schema-record/schema'; | ||
import { module, test } from 'qunit'; | ||
|
||
import { setupTest } from 'ember-qunit'; | ||
|
||
import type Store from '@ember-data/store'; | ||
|
||
interface User { | ||
id: string | null; | ||
$type: 'user'; | ||
firstName: string; | ||
lastName: string; | ||
readonly fullName: string; | ||
} | ||
|
||
module('Reads | derivation', function (hooks) { | ||
setupTest(hooks); | ||
|
||
test('we can use simple fields with no `type`', function (assert) { | ||
const store = this.owner.lookup('service:store') as Store; | ||
const schema = new SchemaService(); | ||
store.registerSchema(schema); | ||
|
||
function concat( | ||
record: SchemaRecord & { [key: string]: unknown }, | ||
options: Record<string, unknown> | null, | ||
_prop: string | ||
): string { | ||
if (!options) throw new Error(`options is required`); | ||
const opts = options as { fields: string[]; separator?: string }; | ||
return opts.fields.map((field) => record[field]).join(opts.separator ?? ''); | ||
} | ||
|
||
schema.registerDerivation('concat', concat); | ||
|
||
schema.defineSchema('user', [ | ||
{ | ||
name: 'firstName', | ||
type: null, | ||
kind: 'attribute', | ||
}, | ||
{ | ||
name: 'lastName', | ||
type: null, | ||
kind: 'attribute', | ||
}, | ||
{ | ||
name: 'fullName', | ||
type: 'concat', | ||
options: { fields: ['firstName', 'lastName'], separator: ' ' }, | ||
kind: 'derived', | ||
}, | ||
]); | ||
|
||
const record = store.createRecord('user', { firstName: 'Rey', lastName: 'Skybarker' }) as User; | ||
|
||
assert.strictEqual(record.id, null, 'id is accessible'); | ||
assert.strictEqual(record.$type, 'user', '$type is accessible'); | ||
|
||
assert.strictEqual(record.firstName, 'Rey', 'firstName is accessible'); | ||
assert.strictEqual(record.lastName, 'Skybarker', 'lastName is accessible'); | ||
assert.strictEqual(record.fullName, 'Rey Skybarker', 'fullName is accessible'); | ||
}); | ||
}); |