diff --git a/packages/core-types/src/graph.ts b/packages/core-types/src/graph.ts index 89f6218d804..6597f1a047a 100644 --- a/packages/core-types/src/graph.ts +++ b/packages/core-types/src/graph.ts @@ -12,14 +12,51 @@ export interface Graph { destroy(): void; } +/** + * Operations are granular instructions that can be applied to a cache to + * update its state. + * + * They are a bit like a PATCH but with greater context around the specific + * change being requested. + * + * @typedoc + */ export interface Operation { op: string; } +/** + * Replace the current relationship remote state with an entirely + * new state. + * + * Effectively a PUT on the specific field. + * + * > [!Warning] + * > This operation behaves differently when used on a paginated collection. + * > In the paginated case, value is used to update the links and meta of the + * > relationshipObject. + * > If data is present, it is presumed that the data represents the data that + * > would be found on the `related` link in links, and will update the data + * > links, and meta on that page. + * + * @typedoc + */ export interface UpdateRelationshipOperation { op: 'updateRelationship'; + /** + * The resource to operate on + * @typedoc + */ record: StableRecordIdentifier; + /** + * The field on the resource that is being updated. + * @typedoc + */ field: string; + /** + * The new value for the relationship. + * @typedoc + */ value: SingleResourceRelationship | CollectionResourceRelationship; } diff --git a/packages/diagnostic/server/bun/watch.js b/packages/diagnostic/server/bun/watch.js index e6be7cc4cc0..fe2e0a35dba 100644 --- a/packages/diagnostic/server/bun/watch.js +++ b/packages/diagnostic/server/bun/watch.js @@ -1,30 +1,52 @@ import { watch } from 'fs'; - -export function addCloseHandler(cb) { +import { debug } from '../utils/debug.js'; +export function addCloseHandler(cb, options) { let executed = false; + const exit = options?.exit + ? (signal) => { + debug(`Exiting with signal ${signal} in CloseHandler for ${options.label ?? ''}`); + const code = typeof signal === 'number' ? signal : 1; + // eslint-disable-next-line n/no-process-exit + process.exit(code); + } + : (signal) => { + debug(`Ignoring signal ${signal} in CloseHandler for ${options.label ?? ''}`); + }; - process.on('SIGINT', () => { - if (executed) return; + process.on('SIGINT', (signal) => { + debug(`CloseHandler for ${options.label ?? ''} Received SIGINT`); + if (executed) return exit(signal); + debug('Executing Close Handler for SIGINT'); executed = true; cb(); + exit(signal); }); - process.on('SIGTERM', () => { - if (executed) return; + process.on('SIGTERM', (signal) => { + debug(`CloseHandler for ${options.label ?? ''} Received SIGTERM`); + if (executed) return exit(signal); + debug('Executing Close Handler for SIGTERM'); executed = true; cb(); + exit(signal); }); - process.on('SIGQUIT', () => { - if (executed) return; + process.on('SIGQUIT', (signal) => { + debug(`CloseHandler for ${options.label ?? ''} Received SIGQUIT`); + if (executed) return exit(signal); + debug('Executing Close Handler for SIGQUIT'); executed = true; cb(); + exit(signal); }); - process.on('exit', () => { - if (executed) return; + process.on('exit', (signal) => { + debug(`CloseHandler for ${options.label ?? ''} Received exit`); + if (executed) return exit(signal); + debug('Executing Close Handler for exit'); executed = true; cb(); + exit(signal); }); } @@ -33,7 +55,13 @@ export function watchAssets(directory, onAssetChange) { onAssetChange(event, filename); }); - addCloseHandler(() => { - watcher.close(); - }); + addCloseHandler( + () => { + watcher.close(); + }, + { + label: 'watchAssets', + exit: false, + } + ); } diff --git a/packages/diagnostic/server/index.js b/packages/diagnostic/server/index.js index b75e3bcd5a4..d720344ea0d 100644 --- a/packages/diagnostic/server/index.js +++ b/packages/diagnostic/server/index.js @@ -5,6 +5,7 @@ import { launchBrowsers } from './bun/launch-browser.js'; import { buildHandler } from './bun/socket-handler.js'; import { debug, error, print } from './utils/debug.js'; import { getPort } from './utils/port.js'; +import { addCloseHandler } from './bun/watch.js'; /** @type {import('bun-types')} */ const isBun = typeof Bun !== 'undefined'; @@ -74,6 +75,20 @@ export default async function launch(config) { debug(`Configured setup hook completed`); } + addCloseHandler( + async () => { + if (config.cleanup) { + debug(`Running configured cleanup hook`); + await config.cleanup(); + debug(`Configured cleanup hook completed`); + } + }, + { + label: 'diagnostic server', + exit: true, + } + ); + await launchBrowsers(config, state); } catch (e) { error(`Error: ${e?.message ?? e}`); diff --git a/packages/diagnostic/src/-types/report.ts b/packages/diagnostic/src/-types/report.ts index 46e8a79f509..67a765a4d86 100644 --- a/packages/diagnostic/src/-types/report.ts +++ b/packages/diagnostic/src/-types/report.ts @@ -30,6 +30,7 @@ export interface TestReport { module: ModuleReport; } export interface ModuleReport { + id: string; name: string; start: PerformanceMark | null; end: PerformanceMark | null; diff --git a/packages/diagnostic/src/internals/run.ts b/packages/diagnostic/src/internals/run.ts index b3985b34d48..d334878692a 100644 --- a/packages/diagnostic/src/internals/run.ts +++ b/packages/diagnostic/src/internals/run.ts @@ -114,6 +114,7 @@ export async function runModule( groupLogs() && console.groupCollapsed(module.name); const moduleReport: ModuleReport = { + id: module.id, name: module.moduleName, start: null, end: null, diff --git a/packages/diagnostic/src/reporters/dom.ts b/packages/diagnostic/src/reporters/dom.ts index 3d580d0f1e9..77edfcd2bcd 100644 --- a/packages/diagnostic/src/reporters/dom.ts +++ b/packages/diagnostic/src/reporters/dom.ts @@ -159,8 +159,9 @@ export class DOMReporter implements Reporter { `${iconForTestStatus(test)} ${labelForTestStatus(test)}`, durationForTest(test), `${test.module.name} > `, - `${test.name} (${test.result.diagnostics.length})`, - getURL(test.id), + `${test.name} (${countPassed(test.result.diagnostics)}/${test.result.diagnostics.length})`, + getTestURL(test.id), + getModuleURL(test.module.id), ]); this.suite.results.set(test, tr); }); @@ -168,11 +169,16 @@ export class DOMReporter implements Reporter { } } +function countPassed(diagnostics: DiagnosticReport[]) { + return diagnostics.filter((d) => d.passed).length; +} + function makeRow(tr: HTMLTableRowElement, cells: string[]) { for (let i = 0; i < cells.length; i++) { const cell = cells[i]; const td = document.createElement('td'); if (i === 3) { + td.className = 'warp-drive__diagnostic-test-name'; const strong = document.createElement('strong'); const text = document.createTextNode(cell); strong.appendChild(text); @@ -183,7 +189,12 @@ function makeRow(tr: HTMLTableRowElement, cells: string[]) { } else if (i === 5) { const a = document.createElement('a'); a.href = cell; - a.appendChild(document.createTextNode('rerun')); + a.appendChild(document.createTextNode('rerun test')); + td.appendChild(a); + } else if (i === 6) { + const a = document.createElement('a'); + a.href = cell; + a.appendChild(document.createTextNode('rerun module')); td.appendChild(a); } else { const text = document.createTextNode(cell); @@ -193,11 +204,17 @@ function makeRow(tr: HTMLTableRowElement, cells: string[]) { } } -function getURL(id: string) { +function getTestURL(id: string) { const currentURL = new URL(window.location.href); currentURL.searchParams.set('t', id); return currentURL.href; } +function getModuleURL(id: string) { + const currentURL = new URL(window.location.href); + currentURL.searchParams.delete('t'); + currentURL.searchParams.set('m', id); + return currentURL.href; +} function durationForTest(test: TestReport) { if (!test.start || !test.end) { @@ -356,6 +373,17 @@ function renderSuite(element: DocumentFragment, suiteReport: SuiteReport): Suite const resultsTable = document.createElement('table'); element.appendChild(resultsTable); + const colGroup = document.createElement('colgroup'); + const colName = document.createElement('col'); + colName.classList.add('warp-drive__diagnostic-test-name'); + colGroup.appendChild(document.createElement('col')); // index + colGroup.appendChild(document.createElement('col')); // status + colGroup.appendChild(document.createElement('col')); // duration + colGroup.appendChild(colName); // name + colGroup.appendChild(document.createElement('col')); // rerun test + colGroup.appendChild(document.createElement('col')); // rerun module + resultsTable.appendChild(colGroup); + const resultsList = document.createElement('tbody'); resultsList.classList.add('diagnostic-results'); resultsTable.appendChild(resultsList); diff --git a/packages/diagnostic/src/styles/dom-reporter.css b/packages/diagnostic/src/styles/dom-reporter.css index 4795727913d..ec1e04b5799 100644 --- a/packages/diagnostic/src/styles/dom-reporter.css +++ b/packages/diagnostic/src/styles/dom-reporter.css @@ -131,6 +131,7 @@ body { } #warp-drive__diagnostic table { + table-layout: fixed; box-sizing: border-box; border-collapse: separate; border-spacing: 0 0.1rem; @@ -142,6 +143,18 @@ body { #warp-drive__diagnostic table td { box-sizing: border-box; } +#warp-drive__diagnostic table col:nth-of-type(1) { + width: 2.5rem; +} +#warp-drive__diagnostic table col:nth-of-type(2) { + width: 2.5rem; +} +#warp-drive__diagnostic table col:nth-of-type(3) { + width: 2.5rem; +} +#warp-drive__diagnostic table .warp-drive__diagnostic-test-name { + width: calc(100vw - 16rem); +} #warp-drive__diagnostic table tr { box-sizing: border-box; @@ -167,6 +180,8 @@ body { #warp-drive__diagnostic table tr td:first-of-type { border-radius: 0.1rem 0 0 0.1rem; padding-left: 0.5rem; + font-size: 0.6em; + line-height: 0.6rem; } #warp-drive__diagnostic table tr td:last-of-type { padding-right: 0.5rem; @@ -178,6 +193,10 @@ body { #warp-drive__diagnostic table tr:nth-last-of-type(odd) { background: rgba(0, 0, 0, 0.8); } +#warp-drive__diagnostic table tr.passed td:nth-child(2) { + font-size: 0.6em; + line-height: 0.6em; +} #warp-drive__diagnostic table tr.passed td:nth-child(2) { color: green; } @@ -192,6 +211,15 @@ body { } #warp-drive__diagnostic table td:nth-child(3) { color: yellow; - font-size: 0.75em; - line-height: 0.75rem; + font-size: 0.6em; + line-height: 0.6rem; +} +#warp-drive__diagnostic table tr.passed td:nth-child(4) { + font-size: 0.85em; + line-height: 1.5em; +} +#warp-drive__diagnostic table tr.passed td:nth-child(5), +#warp-drive__diagnostic table tr.passed td:nth-child(6) { + font-size: 0.6em; + line-height: 0.6em; } diff --git a/packages/graph/package.json b/packages/graph/package.json index 3ff84a84d67..c0053264cbe 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -15,6 +15,7 @@ "scripts": { "lint": "eslint . --quiet --cache --cache-strategy=content --report-unused-disable-directives", "build:pkg": "vite build;", + "start": "vite build --watch;", "prepack": "bun run build:pkg", "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, diff --git a/packages/graph/src/-private.ts b/packages/graph/src/-private.ts index 5b0b2d75d87..f7421b647d7 100644 --- a/packages/graph/src/-private.ts +++ b/packages/graph/src/-private.ts @@ -33,9 +33,9 @@ import { DEBUG } from '@warp-drive/build-config/env'; import { getStore } from './-private/-utils'; import { Graph, Graphs } from './-private/graph'; -export { isBelongsTo } from './-private/-utils'; -export type { CollectionEdge } from './-private/edges/collection'; -export type { ResourceEdge } from './-private/edges/resource'; +export { isBelongsToEdge as isBelongsTo } from './-private/-utils'; +export type { LegacyHasManyEdge as CollectionEdge } from './-private/edges/has-many'; +export type { LegacyBelongsToEdge as ResourceEdge } from './-private/edges/belongs-to'; export type { ImplicitEdge } from './-private/edges/implicit'; export type { GraphEdge } from './-private/graph'; export type { UpgradedMeta } from './-private/-edge-definition'; diff --git a/packages/graph/src/-private/-diff.ts b/packages/graph/src/-private/-diff.ts index 71a42cd60ee..e3d88ec292e 100644 --- a/packages/graph/src/-private/-diff.ts +++ b/packages/graph/src/-private/-diff.ts @@ -5,10 +5,10 @@ import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import { isBelongsTo } from './-utils'; +import { isBelongsToEdge } from './-utils'; import { assertPolymorphicType } from './debug/assert-polymorphic-type'; -import type { CollectionEdge } from './edges/collection'; -import type { ResourceEdge } from './edges/resource'; +import type { LegacyBelongsToEdge } from './edges/belongs-to'; +import type { LegacyHasManyEdge } from './edges/has-many'; import type { Graph } from './graph'; import replaceRelatedRecord from './operations/replace-related-record'; import replaceRelatedRecords from './operations/replace-related-records'; @@ -164,7 +164,7 @@ type Diff = { export function diffCollection( finalState: StableRecordIdentifier[], - relationship: CollectionEdge, + relationship: LegacyHasManyEdge, onAdd: (v: StableRecordIdentifier) => void, onDel: (v: StableRecordIdentifier) => void ): Diff { @@ -202,7 +202,7 @@ export function diffCollection( return _compare(finalState, finalSet, remoteState, remoteMembers, onAdd, onDel); } -export function computeLocalState(storage: CollectionEdge): StableRecordIdentifier[] { +export function computeLocalState(storage: LegacyHasManyEdge): StableRecordIdentifier[] { if (!storage.isDirty) { assert(`Expected localState to be present`, Array.isArray(storage.localState)); return storage.localState; @@ -227,7 +227,7 @@ export function computeLocalState(storage: CollectionEdge): StableRecordIdentifi export function _addLocal( graph: Graph, record: StableRecordIdentifier, - relationship: CollectionEdge, + relationship: LegacyHasManyEdge, value: StableRecordIdentifier, index: number | null ): boolean { @@ -284,7 +284,7 @@ export function _addLocal( return true; } -export function _removeLocal(relationship: CollectionEdge, value: StableRecordIdentifier): boolean { +export function _removeLocal(relationship: LegacyHasManyEdge, value: StableRecordIdentifier): boolean { assert(`expected an identifier to remove from the collection relationship`, value); const { remoteMembers, additions } = relationship; let removals = relationship.removals; @@ -327,7 +327,7 @@ export function _removeLocal(relationship: CollectionEdge, value: StableRecordId return true; } -export function _removeRemote(relationship: CollectionEdge, value: StableRecordIdentifier): boolean { +export function _removeRemote(relationship: LegacyHasManyEdge, value: StableRecordIdentifier): boolean { assert(`expected an identifier to remove from the collection relationship`, value); const { remoteMembers, additions, removals, remoteState } = relationship; @@ -379,9 +379,9 @@ export function rollbackRelationship( graph: Graph, identifier: StableRecordIdentifier, field: string, - relationship: CollectionEdge | ResourceEdge + relationship: LegacyHasManyEdge | LegacyBelongsToEdge ): void { - if (isBelongsTo(relationship)) { + if (isBelongsToEdge(relationship)) { replaceRelatedRecord( graph, { diff --git a/packages/graph/src/-private/-edge-definition.ts b/packages/graph/src/-private/-edge-definition.ts index 6412cbb0400..d0d93c3875d 100644 --- a/packages/graph/src/-private/-edge-definition.ts +++ b/packages/graph/src/-private/-edge-definition.ts @@ -26,19 +26,12 @@ export function isLegacyField(field: FieldSchema): field is LegacyBelongsToField return field.kind === 'belongsTo' || field.kind === 'hasMany'; } -export function isRelationshipField(field: FieldSchema): field is RelationshipField { - return RELATIONSHIP_KINDS.includes(field.kind); +export function isModernField(field: FieldSchema): field is ResourceField | CollectionField { + return field.kind === 'resource' || field.kind === 'collection'; } -export function temporaryConvertToLegacy( - field: ResourceField | CollectionField -): LegacyBelongsToField | LegacyHasManyField { - return { - kind: field.kind === 'resource' ? 'belongsTo' : 'hasMany', - name: field.name, - type: field.type, - options: Object.assign({}, { async: false, inverse: null, resetOnRemoteUpdate: false as const }, field.options), - }; +export function isRelationshipField(field: FieldSchema): field is RelationshipField { + return RELATIONSHIP_KINDS.includes(field.kind); } /** @@ -69,6 +62,7 @@ export function temporaryConvertToLegacy( * isAsync: false, * isImplicit: false, * isCollection: true, + * isPaginated: false, * isPolymorphic: false, * inverseKind: 'belongsTo', * inverseKey: 'owner', @@ -76,6 +70,7 @@ export function temporaryConvertToLegacy( * inverseIsAsync: false, * inverseIsImplicit: false, * inverseIsCollection: false, + * inverseIsPaginated: false, * inverseIsPolymorphic: false, * } * ``` @@ -90,6 +85,7 @@ export function temporaryConvertToLegacy( * isAsync: false, * isImplicit: false, * isCollection: false, + * isPaginated: false, * isPolymorphic: false, * inverseKind: 'hasMany', * inverseKey: 'pets', @@ -97,6 +93,7 @@ export function temporaryConvertToLegacy( * inverseIsAsync: false, * inverseIsImplicit: false, * inverseIsCollection: true, + * inverseIsPaginated: false, * inverseIsPolymorphic: false, * } * ``` @@ -122,6 +119,7 @@ export interface UpgradedMeta { isAsync: boolean; isImplicit: boolean; isCollection: boolean; + isPaginated: boolean; isPolymorphic: boolean; resetOnRemoteUpdate: boolean; @@ -139,6 +137,7 @@ export interface UpgradedMeta { inverseIsAsync: boolean; inverseIsImplicit: boolean; inverseIsCollection: boolean; + inverseIsPaginated: boolean; inverseIsPolymorphic: boolean; } @@ -193,6 +192,7 @@ function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) { definition.inverseType = inverseDefinition.type; definition.inverseIsAsync = inverseDefinition.isAsync; definition.inverseIsCollection = inverseDefinition.isCollection; + definition.inverseIsPaginated = inverseDefinition.isPaginated; definition.inverseIsPolymorphic = inverseDefinition.isPolymorphic; definition.inverseIsImplicit = inverseDefinition.isImplicit; const resetOnRemoteUpdate = @@ -202,25 +202,27 @@ function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) { } function upgradeMeta(meta: RelationshipField): UpgradedMeta { - if (!isLegacyField(meta)) { - meta = temporaryConvertToLegacy(meta); - } const niceMeta: UpgradedMeta = {} as UpgradedMeta; const options = meta.options; niceMeta.kind = meta.kind; niceMeta.key = meta.name; niceMeta.type = meta.type; - assert(`Expected relationship definition to specify async`, typeof options?.async === 'boolean'); - niceMeta.isAsync = options.async; + assert( + `Expected relationship definition to specify async`, + isModernField(meta) || typeof meta.options?.async === 'boolean' + ); + niceMeta.isAsync = isModernField(meta) ? Boolean(meta.options?.async) : meta.options.async; niceMeta.isImplicit = false; - niceMeta.isCollection = meta.kind === 'hasMany'; - niceMeta.isPolymorphic = options && !!options.polymorphic; + niceMeta.isCollection = meta.kind === 'hasMany' || meta.kind === 'collection'; + niceMeta.isPolymorphic = Boolean(options && options.polymorphic); + niceMeta.isPaginated = meta.kind === 'collection'; niceMeta.inverseKey = (options && options.inverse) || STR_LATER; niceMeta.inverseType = STR_LATER; niceMeta.inverseIsAsync = BOOL_LATER; niceMeta.inverseIsImplicit = (options && options.inverse === null) || BOOL_LATER; niceMeta.inverseIsCollection = BOOL_LATER; + niceMeta.inverseIsPaginated = BOOL_LATER; niceMeta.resetOnRemoteUpdate = isLegacyField(meta) ? meta.options?.resetOnRemoteUpdate === false @@ -369,7 +371,7 @@ export function isRHS(info: EdgeDefinition, type: string, key: string): boolean export function upgradeDefinition( graph: Graph, - identifier: StableRecordIdentifier, + identifier: { type: string }, propertyName: string, isImplicit = false ): EdgeDefinition | null { @@ -429,6 +431,8 @@ export function upgradeDefinition( // TODO probably dont need this assertion if polymorphic assert(`Expected the inverse model to exist`, getStore(storeWrapper).modelFor(inverseType)); inverseDefinition = null; + } else if (definition.isCollection && !definition.inverseKey) { + inverseDefinition = null; } else { inverseKey = /*#__NOINLINE__*/ inverseForRelationship(getStore(storeWrapper), identifier, propertyName); @@ -444,6 +448,7 @@ export function upgradeDefinition( isAsync: false, // this must be updated when we find the first belongsTo or hasMany definition that matches isImplicit: false, isCollection: false, // this must be updated when we find the first belongsTo or hasMany definition that matches + isPaginated: false, isPolymorphic: false, } as UpgradedMeta; // the rest of the fields are populated by syncMeta @@ -476,6 +481,7 @@ export function upgradeDefinition( isAsync: false, isImplicit: true, isCollection: true, // with implicits any number of records could point at us + isPaginated: false, isPolymorphic: false, } as UpgradedMeta; // the rest of the fields are populated by syncMeta @@ -570,6 +576,23 @@ export function upgradeDefinition( isReflexive: isSelfReferential && propertyName === inverseKey, }; + assert( + `a belongsTo relationship can only relate to a belongsTo or hasMany relationship`, + definition.kind !== 'belongsTo' || ['belongsTo', 'hasMany', 'implicit'].includes(definition.inverseKind) + ); + assert( + `a hasMany relationship can only relate to a belongsTo or hasMany relationship`, + definition.kind !== 'hasMany' || ['belongsTo', 'hasMany', 'implicit'].includes(definition.inverseKind) + ); + assert( + `a resource relationship can only relate to a resource or collection relationship`, + definition.kind !== 'resource' || ['resource', 'collection', 'implicit'].includes(definition.inverseKind) + ); + assert( + `a collection relationship can only relate to a resource or collection relationship`, + definition.kind !== 'collection' || ['resource', 'collection', 'implicit'].includes(definition.inverseKind) + ); + // Create entries for the baseModelName as well as modelName to speed up // inverse lookups expandingSet(cache, baseType, propertyName, info); @@ -588,6 +611,11 @@ function inverseForRelationship(store: Store, identifier: StableRecordIdentifier } assert(`Expected ${key} to be a relationship`, isRelationshipField(definition)); + + if (definition.kind === 'collection' || definition.kind === 'resource') { + return definition.options?.inverse || null; + } + assert( `Expected the relationship defintion to specify the inverse type or null.`, definition.options?.inverse === null || diff --git a/packages/graph/src/-private/-utils.ts b/packages/graph/src/-private/-utils.ts index ccdf4aa740d..e255be85b0a 100644 --- a/packages/graph/src/-private/-utils.ts +++ b/packages/graph/src/-private/-utils.ts @@ -11,7 +11,9 @@ import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/json- import type { UpgradedMeta } from './-edge-definition'; import { coerceId } from './coerce-id'; +import type { LegacyBelongsToEdge } from './edges/belongs-to'; import type { CollectionEdge } from './edges/collection'; +import type { LegacyHasManyEdge } from './edges/has-many'; import type { ImplicitEdge } from './edges/implicit'; import type { ResourceEdge } from './edges/resource'; import type { Graph, GraphEdge } from './graph'; @@ -33,7 +35,7 @@ export function expandingSet(cache: Record>, key1: export function assertValidRelationshipPayload(graph: Graph, op: UpdateRelationshipOperation) { const relationship = graph.get(op.record, op.field); - assert(`Cannot update an implicit relationship`, isHasMany(relationship) || isBelongsTo(relationship)); + assert(`Cannot update an implicit relationship`, !isImplicitEdge(relationship)); const payload = op.value; const { definition, identifier, state } = relationship; const { type } = identifier; @@ -81,27 +83,35 @@ export function isNew(identifier: StableRecordIdentifier): boolean { return Boolean(cache?.isNew(identifier)); } -export function isBelongsTo(relationship: GraphEdge): relationship is ResourceEdge { +export function isResourceEdge(relationship: GraphEdge): relationship is ResourceEdge { + return relationship.definition.kind === 'resource'; +} + +export function isCollectionEdge(relationship: GraphEdge): relationship is CollectionEdge { + return relationship.definition.kind === 'collection'; +} + +export function isBelongsToEdge(relationship: GraphEdge): relationship is LegacyBelongsToEdge { return relationship.definition.kind === 'belongsTo'; } -export function isImplicit(relationship: GraphEdge): relationship is ImplicitEdge { +export function isImplicitEdge(relationship: GraphEdge): relationship is ImplicitEdge { return relationship.definition.isImplicit; } -export function isHasMany(relationship: GraphEdge): relationship is CollectionEdge { +export function isHasManyEdge(relationship: GraphEdge): relationship is LegacyHasManyEdge { return relationship.definition.kind === 'hasMany'; } export function forAllRelatedIdentifiers(rel: GraphEdge, cb: (identifier: StableRecordIdentifier) => void): void { - if (isBelongsTo(rel)) { + if (isBelongsToEdge(rel) || isResourceEdge(rel)) { if (rel.remoteState) { cb(rel.remoteState); } if (rel.localState && rel.localState !== rel.remoteState) { cb(rel.localState); } - } else if (isHasMany(rel)) { + } else if (isHasManyEdge(rel)) { // TODO // rel.remoteMembers.forEach(cb); // might be simpler if performance is not a concern @@ -110,6 +120,8 @@ export function forAllRelatedIdentifiers(rel: GraphEdge, cb: (identifier: Stable cb(inverseIdentifier); } rel.additions?.forEach(cb); + } else if (isCollectionEdge(rel)) { + throw new Error('Not implemented'); } else { rel.localMembers.forEach(cb); rel.remoteMembers.forEach((inverseIdentifier) => { @@ -132,7 +144,7 @@ export function removeIdentifierCompletelyFromRelationship( value: StableRecordIdentifier, silenceNotifications?: boolean ): void { - if (isBelongsTo(relationship)) { + if (isBelongsToEdge(relationship)) { if (relationship.remoteState === value) { relationship.remoteState = null; } @@ -146,7 +158,7 @@ export function removeIdentifierCompletelyFromRelationship( notifyChange(graph, relationship.identifier, relationship.definition.key); } } - } else if (isHasMany(relationship)) { + } else if (isHasManyEdge(relationship)) { relationship.remoteMembers.delete(value); relationship.additions?.delete(value); const wasInRemovals = relationship.removals?.delete(value); @@ -168,9 +180,11 @@ export function removeIdentifierCompletelyFromRelationship( } } } - } else { + } else if (isImplicitEdge(relationship)) { relationship.remoteMembers.delete(value); relationship.localMembers.delete(value); + } else { + throw new Error('Not implemented'); } } diff --git a/packages/graph/src/-private/debug/assert-polymorphic-type.ts b/packages/graph/src/-private/debug/assert-polymorphic-type.ts index 1855d91ea8f..636608890e3 100644 --- a/packages/graph/src/-private/debug/assert-polymorphic-type.ts +++ b/packages/graph/src/-private/debug/assert-polymorphic-type.ts @@ -3,8 +3,14 @@ import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { + CollectionField, + LegacyBelongsToField, + LegacyHasManyField, + ResourceField, +} from '@warp-drive/core-types/schema/fields'; -import { isLegacyField, isRelationshipField, temporaryConvertToLegacy, type UpgradedMeta } from '../-edge-definition'; +import { isLegacyField, isRelationshipField, type UpgradedMeta } from '../-edge-definition'; /* Assert that `addedRecord` has a valid type so it can be added to the @@ -39,16 +45,18 @@ if (DEBUG) { if (definition.inverseKind !== meta.kind) { errors.set('type', ` <---- should be '${definition.inverseKind}'`); } - if (definition.inverseIsAsync !== meta.options.async) { - errors.set('async', ` <---- should be ${definition.inverseIsAsync}`); + if (definition.kind !== 'collection') { + if (definition.inverseIsAsync !== meta.options?.async) { + errors.set('async', ` <---- should be ${definition.inverseIsAsync}`); + } } - if (definition.inverseIsPolymorphic && definition.inverseIsPolymorphic !== meta.options.polymorphic) { + if (definition.inverseIsPolymorphic && definition.inverseIsPolymorphic !== meta.options?.polymorphic) { errors.set('polymorphic', ` <---- should be ${definition.inverseIsPolymorphic}`); } - if (definition.key !== meta.options.inverse) { + if (definition.key !== meta.options?.inverse) { errors.set('inverse', ` <---- should be '${definition.key}'`); } - if (definition.type !== meta.options.as) { + if (definition.type !== meta.options?.as) { errors.set('as', ` <---- should be '${definition.type}'`); } @@ -59,15 +67,24 @@ if (DEBUG) { name: string; type: string; kind: string; - options: { + options?: { as?: string; - async: boolean; + async?: boolean; polymorphic?: boolean; - inverse: string | null; + inverse?: string | null; }; }; type RelationshipSchemaError = 'name' | 'type' | 'kind' | 'as' | 'async' | 'polymorphic' | 'inverse'; + function temporaryConvertToLegacy(field: ResourceField | CollectionField): LegacyBelongsToField | LegacyHasManyField { + return { + kind: field.kind === 'resource' ? 'belongsTo' : 'hasMany', + name: field.name, + type: field.type, + options: Object.assign({}, { async: false, inverse: null, resetOnRemoteUpdate: false as const }, field.options), + }; + } + function expectedSchema(definition: UpgradedMeta) { return printSchema({ name: definition.inverseKey, @@ -83,6 +100,19 @@ if (DEBUG) { } function printSchema(config: PrintConfig, errors?: Map) { + const optionLines = [ + config.options?.as ? ` as: '${config.options.as}',${errors?.get('as') || ''}` : '', + config.kind !== 'collection' ? ` async: ${config.options?.async},${errors?.get('async') || ''}` : '', + config.options?.polymorphic || errors?.get('polymorphic') + ? ` polymorphic: ${config.options?.polymorphic},${errors?.get('polymorphic') || ''}` + : '', + config.kind !== 'collection' || errors?.get('inverse') + ? ` inverse: '${config.options?.inverse}'${errors?.get('inverse') || ''}` + : '', + ] + .filter(Boolean) + .join('\n'); + return ` \`\`\` @@ -92,10 +122,7 @@ if (DEBUG) { type: '${config.type}',${errors?.get('type') || ''} kind: '${config.kind}',${errors?.get('kind') || ''} options: { - as: '${config.options.as}',${errors?.get('as') || ''} - async: ${config.options.async},${errors?.get('async') || ''} - polymorphic: ${config.options.polymorphic},${errors?.get('polymorphic') || ''} - inverse: '${config.options.inverse}'${errors?.get('inverse') || ''} +${optionLines} } } } @@ -137,6 +164,7 @@ if (DEBUG) { isAsync: definition.inverseIsAsync, isPolymorphic: true, isCollection: definition.inverseIsCollection, + isPaginated: definition.inverseIsPaginated, isImplicit: definition.inverseIsImplicit, inverseKey: definition.key, inverseType: definition.type, @@ -145,6 +173,7 @@ if (DEBUG) { inverseIsPolymorphic: definition.isPolymorphic, inverseIsImplicit: definition.isImplicit, inverseIsCollection: definition.isCollection, + inverseIsPaginated: definition.isPaginated, resetOnRemoteUpdate: definition.resetOnRemoteUpdate, }; } @@ -230,7 +259,7 @@ if (DEBUG) { meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); assert( `You should not specify both options.as and options.inverse as null on ${addedIdentifier.type}.${parentDefinition.inverseKey}, as if there is no inverse field there is no abstract type to conform to. You may have intended for this relationship to be polymorphic, or you may have mistakenly set inverse to null.`, - !(meta.options.inverse === null && meta?.options.as?.length) + !(meta.options?.inverse === null && meta.options.as?.length) ); const errors = validateSchema(parentDefinition, meta); assert( diff --git a/packages/graph/src/-private/edges/belongs-to.ts b/packages/graph/src/-private/edges/belongs-to.ts new file mode 100644 index 00000000000..cf60779b034 --- /dev/null +++ b/packages/graph/src/-private/edges/belongs-to.ts @@ -0,0 +1,67 @@ +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { Links, Meta } from '@warp-drive/core-types/spec/json-api-raw'; + +import type { UpgradedMeta } from '../-edge-definition'; +import type { RelationshipState } from '../-state'; +import { createState } from '../-state'; + +/** + * Stores the data for one side of a "single" resource relationship. + * + * @typedoc + */ +export interface LegacyBelongsToEdge { + definition: UpgradedMeta & { kind: 'belongsTo' }; + identifier: StableRecordIdentifier; + state: RelationshipState; + localState: StableRecordIdentifier | null; + remoteState: StableRecordIdentifier | null; + meta: Meta | null; + links: Links | null; + transactionRef: number; +} + +export function isLegacyBelongsToKind(definition: UpgradedMeta): definition is UpgradedMeta & { kind: 'belongsTo' } { + return definition.kind === 'belongsTo'; +} + +export function createLegacyBelongsToEdge( + definition: UpgradedMeta, + identifier: StableRecordIdentifier +): LegacyBelongsToEdge { + assert(`Expected a belongsTo relationship`, isLegacyBelongsToKind(definition)); + return { + definition, + identifier, + state: createState(), + transactionRef: 0, + localState: null, + remoteState: null, + meta: null, + links: null, + }; +} + +export function getLegacyBelongsToRelationshipData(source: LegacyBelongsToEdge): ResourceRelationship { + let data: StableRecordIdentifier | null | undefined; + const payload: ResourceRelationship = {}; + if (source.localState) { + data = source.localState; + } + if (source.localState === null && source.state.hasReceivedData) { + data = null; + } + if (source.links) { + payload.links = source.links; + } + if (data !== undefined) { + payload.data = data; + } + if (source.meta) { + payload.meta = source.meta; + } + + return payload; +} diff --git a/packages/graph/src/-private/edges/collection.ts b/packages/graph/src/-private/edges/collection.ts index ed61a0c6a7b..5a5bfdadc4d 100644 --- a/packages/graph/src/-private/edges/collection.ts +++ b/packages/graph/src/-private/edges/collection.ts @@ -1,14 +1,20 @@ +import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; import type { Links, Meta, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; -import { computeLocalState } from '../-diff'; +// import { computeLocalState } from '../-diff'; import type { UpgradedMeta } from '../-edge-definition'; import type { RelationshipState } from '../-state'; import { createState } from '../-state'; +/** + * Stores the data for one side of a "hasMany" relationship. + * + * @typedoc + */ export interface CollectionEdge { - definition: UpgradedMeta; + definition: UpgradedMeta & { kind: 'collection' }; identifier: StableRecordIdentifier; state: RelationshipState; @@ -31,7 +37,12 @@ export interface CollectionEdge { }; } +export function isCollectionKind(definition: UpgradedMeta): definition is UpgradedMeta & { kind: 'collection' } { + return definition.kind === 'collection'; +} + export function createCollectionEdge(definition: UpgradedMeta, identifier: StableRecordIdentifier): CollectionEdge { + assert(`Expected a hasMany relationship`, isCollectionKind(definition)); return { definition, identifier, @@ -51,11 +62,11 @@ export function createCollectionEdge(definition: UpgradedMeta, identifier: Stabl }; } -export function legacyGetCollectionRelationshipData(source: CollectionEdge): CollectionRelationship { +export function getCollectionRelationshipData(source: CollectionEdge): CollectionRelationship { const payload: CollectionRelationship = {}; if (source.state.hasReceivedData) { - payload.data = computeLocalState(source); + // payload.data = computeLocalState(source); } if (source.links) { diff --git a/packages/graph/src/-private/edges/has-many.ts b/packages/graph/src/-private/edges/has-many.ts new file mode 100644 index 00000000000..f98e02c5573 --- /dev/null +++ b/packages/graph/src/-private/edges/has-many.ts @@ -0,0 +1,84 @@ +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { Links, Meta, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; + +import { computeLocalState } from '../-diff'; +import type { UpgradedMeta } from '../-edge-definition'; +import type { RelationshipState } from '../-state'; +import { createState } from '../-state'; + +/** + * Stores the data for one side of a "hasMany" relationship. + * + * @typedoc + */ +export interface LegacyHasManyEdge { + definition: UpgradedMeta & { kind: 'hasMany' }; + identifier: StableRecordIdentifier; + state: RelationshipState; + + remoteMembers: Set; + remoteState: StableRecordIdentifier[]; + + additions: Set | null; + removals: Set | null; + + meta: Meta | null; + links: Links | PaginationLinks | null; + + localState: StableRecordIdentifier[] | null; + isDirty: boolean; + transactionRef: number; + + _diff?: { + add: Set; + del: Set; + }; +} + +export function isLegacyHasManyKind(definition: UpgradedMeta): definition is UpgradedMeta & { kind: 'hasMany' } { + return definition.kind === 'hasMany'; +} + +export function createLegacyHasManyEdge( + definition: UpgradedMeta, + identifier: StableRecordIdentifier +): LegacyHasManyEdge { + assert(`Expected a hasMany relationship`, isLegacyHasManyKind(definition)); + return { + definition, + identifier, + state: createState(), + remoteMembers: new Set(), + remoteState: [], + additions: null, + removals: null, + + meta: null, + links: null, + + localState: null, + isDirty: true, + transactionRef: 0, + _diff: undefined, + }; +} + +export function legacyGetCollectionRelationshipData(source: LegacyHasManyEdge): CollectionRelationship { + const payload: CollectionRelationship = {}; + + if (source.state.hasReceivedData) { + payload.data = computeLocalState(source); + } + + if (source.links) { + payload.links = source.links; + } + + if (source.meta) { + payload.meta = source.meta; + } + + return payload; +} diff --git a/packages/graph/src/-private/edges/paginated-collection.ts b/packages/graph/src/-private/edges/paginated-collection.ts new file mode 100644 index 00000000000..16ab06ed7db --- /dev/null +++ b/packages/graph/src/-private/edges/paginated-collection.ts @@ -0,0 +1,148 @@ +import { assert } from '@ember/debug'; + +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { Meta, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; + +import type { UpgradedMeta } from '../-edge-definition'; + +const Unpaged = Symbol('Unpaged'); +const Local = Symbol('Local'); + +interface CollectionPage { + links: PaginationLinks | null; + meta: Meta | null; + remoteMembers: Set; + isDirty: boolean; + localState: StableRecordIdentifier[] | null; +} + +export interface PaginatedCollectionEdge { + definition: UpgradedMeta; + identifier: StableRecordIdentifier; + // state: RelationshipState; + + pages: Map; + + // remote state added via inverse that is not yet in a page + remoteMembers: Set; + + // locally added records to add to unpaged + additions: Set | null; + + // locally removed records to remove from all pages + removals: Set | null; + + // relationshipObject meta & links + // (as opposed to the meta & links of a specific page) + meta: Meta | null; + links: PaginationLinks | null; + + // a cache of the unpaged data with removals applied + localState: StableRecordIdentifier[] | null; + // whether localState is up-to-date + isDirty: boolean; + transactionRef: number; + + // TODO: this should likely be per-page + _diff?: { + add: Set; + del: Set; + }; +} + +export function createPaginatedCollectionEdge( + definition: UpgradedMeta, + identifier: StableRecordIdentifier +): PaginatedCollectionEdge { + return { + definition, + identifier, + + pages: new Map(), + remoteMembers: new Set(), + + additions: null, + removals: null, + + meta: null, + links: null, + + isDirty: true, + localState: null, + transactionRef: 0, + _diff: undefined, + }; +} + +type PaginatedCollectionRelationship = { + links?: PaginationLinks; + meta?: Meta; + pages: Map; +}; + +function computePageState( + storage: CollectionPage | PaginatedCollectionEdge, + source: PaginatedCollectionEdge +): StableRecordIdentifier[] { + if (!storage.isDirty) { + assert(`Expected localState to be present`, Array.isArray(storage.localState)); + return storage.localState; + } + + let result: StableRecordIdentifier[]; + if (!source.removals?.size) { + result = Array.from(storage.remoteMembers); + } else { + const state = new Set(storage.remoteMembers); + source.removals?.forEach((v) => { + state.delete(v); + }); + result = Array.from(state); + } + + storage.localState = result; + storage.isDirty = false; + + return result; +} + +export function getPaginatedCollectionData(source: PaginatedCollectionEdge): PaginatedCollectionRelationship { + const payload: PaginatedCollectionRelationship = { + pages: new Map(), + }; + + // primary pages + source.pages.forEach((page, key) => { + const collectionRelationship: CollectionRelationship = {}; + collectionRelationship.data = computePageState(page, source); + + if (page.links) { + collectionRelationship.links = page.links; + } + + if (page.meta) { + collectionRelationship.meta = page.meta; + } + + payload.pages.set(key, collectionRelationship); + }); + + // specialized pages + payload.pages.set(Unpaged, { + data: computePageState(source, source), + }); + payload.pages.set(Local, { + data: source.additions ? Array.from(source.additions) : [], + }); + + if (source.links) { + payload.links = source.links; + } + + if (source.meta) { + payload.meta = source.meta; + } + + return payload; +} diff --git a/packages/graph/src/-private/edges/resource.ts b/packages/graph/src/-private/edges/resource.ts index 3d5c615335a..7d89b84bae2 100644 --- a/packages/graph/src/-private/edges/resource.ts +++ b/packages/graph/src/-private/edges/resource.ts @@ -1,31 +1,33 @@ +import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; -import type { Links, Meta, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; +import type { Links, Meta } from '@warp-drive/core-types/spec/json-api-raw'; import type { UpgradedMeta } from '../-edge-definition'; import type { RelationshipState } from '../-state'; import { createState } from '../-state'; - -/* - * @module @ember-data/graph - * +/** * Stores the data for one side of a "single" resource relationship. * - * @class ResourceEdge - * @internal + * @typedoc */ export interface ResourceEdge { - definition: UpgradedMeta; + definition: UpgradedMeta & { kind: 'resource' }; identifier: StableRecordIdentifier; state: RelationshipState; localState: StableRecordIdentifier | null; remoteState: StableRecordIdentifier | null; meta: Meta | null; - links: Links | PaginationLinks | null; + links: Links | null; transactionRef: number; } +export function isResourceKind(definition: UpgradedMeta): definition is UpgradedMeta & { kind: 'resource' } { + return definition.kind === 'resource'; +} + export function createResourceEdge(definition: UpgradedMeta, identifier: StableRecordIdentifier): ResourceEdge { + assert(`Expected a resource relationship`, isResourceKind(definition)); return { definition, identifier, @@ -38,7 +40,7 @@ export function createResourceEdge(definition: UpgradedMeta, identifier: StableR }; } -export function legacyGetResourceRelationshipData(source: ResourceEdge): ResourceRelationship { +export function getResourceRelationshipData(source: ResourceEdge): ResourceRelationship { let data: StableRecordIdentifier | null | undefined; const payload: ResourceRelationship = {}; if (source.localState) { diff --git a/packages/graph/src/-private/graph.ts b/packages/graph/src/-private/graph.ts index 6a9a441486d..bcaf3f5d6cb 100644 --- a/packages/graph/src/-private/graph.ts +++ b/packages/graph/src/-private/graph.ts @@ -21,34 +21,62 @@ import { assertValidRelationshipPayload, forAllRelatedIdentifiers, getStore, - isBelongsTo, - isHasMany, - isImplicit, + isBelongsToEdge, + isCollectionEdge, + isHasManyEdge, + isImplicitEdge, isNew, + isResourceEdge, notifyChange, removeIdentifierCompletelyFromRelationship, } from './-utils'; -import { type CollectionEdge, createCollectionEdge, legacyGetCollectionRelationshipData } from './edges/collection'; +import { + createLegacyBelongsToEdge, + getLegacyBelongsToRelationshipData, + type LegacyBelongsToEdge, +} from './edges/belongs-to'; +import type { CollectionEdge } from './edges/collection'; +import { createLegacyHasManyEdge, legacyGetCollectionRelationshipData, type LegacyHasManyEdge } from './edges/has-many'; import type { ImplicitEdge, ImplicitMeta } from './edges/implicit'; import { createImplicitEdge } from './edges/implicit'; -import { createResourceEdge, legacyGetResourceRelationshipData, type ResourceEdge } from './edges/resource'; +import type { ResourceEdge } from './edges/resource'; +import { createResourceEdge, getResourceRelationshipData } from './edges/resource'; import addToRelatedRecords from './operations/add-to-related-records'; import { mergeIdentifier } from './operations/merge-identifier'; import removeFromRelatedRecords from './operations/remove-from-related-records'; import replaceRelatedRecord from './operations/replace-related-record'; import replaceRelatedRecords from './operations/replace-related-records'; -import updateRelationshipOperation from './operations/update-relationship'; +import { updateRelationshipOperation } from './operations/update-relationship'; -export type GraphEdge = ImplicitEdge | CollectionEdge | ResourceEdge; +export type GraphEdge = ImplicitEdge | LegacyHasManyEdge | LegacyBelongsToEdge | ResourceEdge | CollectionEdge; export const Graphs = getOrSetGlobal('Graphs', new Map()); type PendingOps = { - belongsTo?: Map>; - hasMany?: Map>; + single?: Map>; + multi?: Map>; deletions: DeleteRecordOperation[]; }; +export function isSingleKind(kind: UpgradedMeta['kind']): kind is 'belongsTo' | 'resource' { + return kind === 'belongsTo' || kind === 'resource'; +} + +function isSingleEdge(edge: GraphEdge): edge is LegacyBelongsToEdge | ResourceEdge { + return isBelongsToEdge(edge) || isResourceEdge(edge); +} + +export function isMultiKind(kind: UpgradedMeta['kind']): kind is 'hasMany' | 'collection' { + return kind === 'hasMany' || kind === 'collection'; +} + +function updateTypeForKind(kind: UpgradedMeta['kind']): 'single' | 'multi' { + if (isSingleKind(kind)) { + return 'single'; + } + return 'multi'; +} + /* * Graph acts as the cache for relationship data. It allows for * us to ask about and update relationships for a given Identifier @@ -79,7 +107,7 @@ export class Graph { declare _willSyncLocal: boolean; declare silenceNotifications: boolean; declare _pushedUpdates: PendingOps; - declare _updatedRelationships: Set; + declare _updatedRelationships: Set; declare _transaction: number | null; declare _removing: StableRecordIdentifier | null; @@ -93,8 +121,8 @@ export class Graph { this._willSyncRemote = false; this._willSyncLocal = false; this._pushedUpdates = { - belongsTo: undefined, - hasMany: undefined, + single: undefined, + multi: undefined, deletions: [], }; this._updatedRelationships = new Set(); @@ -111,7 +139,20 @@ export class Graph { return relationships[propertyName] !== undefined; } - getDefinition(identifier: StableRecordIdentifier, propertyName: string): UpgradedMeta { + /** + * Retrieves the relationship schema for the record identified by `identifier` + * and the relationship `propertyName`. + * + * This method will cache the relationship schema for future lookups. + * + * Relationship schemas are derived from relationship FieldSchemas such as + * LegacyHasManyField, LegacyBelongsToField, ResourceField, and CollectionField, + * but are upgraded to contain more complete information about both sides of the + * relationship. + * + * @typedoc + */ + getDefinition(identifier: { type: string }, propertyName: string): UpgradedMeta { let defs = this._metaCache[identifier.type]; let meta: UpgradedMeta | null | undefined = defs?.[propertyName]; if (!meta) { @@ -131,6 +172,14 @@ export class Graph { return meta; } + /** + * Retrieves the cache storage for the relationship `propertyName` on the record + * identified by `identifier`. + * + * If the storage does not exist, it will be created. + * + * @typedoc + */ get(identifier: StableRecordIdentifier, propertyName: string): GraphEdge { assert(`expected propertyName`, propertyName); let relationships = this.identifiers.get(identifier); @@ -144,9 +193,13 @@ export class Graph { const meta = this.getDefinition(identifier, propertyName); if (meta.kind === 'belongsTo') { + relationship = relationships[propertyName] = createLegacyBelongsToEdge(meta, identifier); + } else if (meta.kind === 'resource') { relationship = relationships[propertyName] = createResourceEdge(meta, identifier); } else if (meta.kind === 'hasMany') { - relationship = relationships[propertyName] = createCollectionEdge(meta, identifier); + relationship = relationships[propertyName] = createLegacyHasManyEdge(meta, identifier); + } else if (meta.kind === 'collection') { + assert(`Collection is not yet implemented`); } else { assert(`Expected kind to be implicit`, meta.kind === 'implicit' && meta.isImplicit === true); relationship = relationships[propertyName] = createImplicitEdge(meta as ImplicitMeta, identifier); @@ -156,19 +209,36 @@ export class Graph { return relationship; } + /** + * Retrieves the cached data for the relationship `propertyName` on the record + * in a Document format. + * + * Any local mutations to the relationship will be applied to the data returned. + * + * To retrieve the raw data without local mutations applied, use either `get` + * or `getChanged` + * + * @typedoc + */ getData(identifier: StableRecordIdentifier, propertyName: string): ResourceRelationship | CollectionRelationship { const relationship = this.get(identifier, propertyName); - assert(`Cannot getData() on an implicit relationship`, !isImplicit(relationship)); + assert(`Cannot getData() on an implicit relationship`, !isImplicitEdge(relationship)); - if (isBelongsTo(relationship)) { - return legacyGetResourceRelationshipData(relationship); + if (isBelongsToEdge(relationship)) { + return getLegacyBelongsToRelationshipData(relationship); + } else if (isHasManyEdge(relationship)) { + return legacyGetCollectionRelationshipData(relationship); + } else if (isResourceEdge(relationship)) { + return getResourceRelationshipData(relationship); + } else { + assert(`Expected a collection relationship`, isCollectionEdge(relationship)); + throw new Error('not implemented'); + // return getCollectionRelationshipData(relationship); } - - return legacyGetCollectionRelationshipData(relationship); } - /* + /** * Allows for the graph to dynamically discover polymorphic connections * without needing to walk prototype chains. * @@ -178,6 +248,8 @@ export class Graph { * Currently we assert before calling this. For a public API we will want * to call out to the schema manager to ask if we should consider these * types as equivalent for a given relationship. + * + * @internal */ registerPolymorphicType(type1: string, type2: string): void { const typeCache = this._potentialPolymorphicTypes; @@ -273,7 +345,7 @@ export class Graph { return; } /*#__NOINLINE__*/ destroyRelationship(this, rel, silenceNotifications); - if (/*#__NOINLINE__*/ isImplicit(rel)) { + if (/*#__NOINLINE__*/ isImplicitEdge(rel)) { // @ts-expect-error relationships[key] = undefined; } @@ -290,9 +362,9 @@ export class Graph { if (!relationship) { return false; } - if (isBelongsTo(relationship)) { + if (isSingleEdge(relationship)) { return relationship.localState !== relationship.remoteState; - } else if (isHasMany(relationship)) { + } else if (isHasManyEdge(relationship)) { const hasAdditions = relationship.additions !== null && relationship.additions.size > 0; const hasRemovals = relationship.removals !== null && relationship.removals.size > 0; return hasAdditions || hasRemovals || isReordered(relationship); @@ -315,7 +387,7 @@ export class Graph { if (!relationship) { continue; } - if (isBelongsTo(relationship)) { + if (isSingleEdge(relationship)) { if (relationship.localState !== relationship.remoteState) { changed.set(field, { kind: 'resource', @@ -323,7 +395,7 @@ export class Graph { localState: relationship.localState, }); } - } else if (isHasMany(relationship)) { + } else if (isHasManyEdge(relationship)) { const hasAdditions = relationship.additions !== null && relationship.additions.size > 0; const hasRemovals = relationship.removals !== null && relationship.removals.size > 0; const reordered = isReordered(relationship); @@ -373,7 +445,7 @@ export class Graph { } if (this._isDirty(identifier, field)) { - rollbackRelationship(this, identifier, field, relationship as CollectionEdge | ResourceEdge); + rollbackRelationship(this, identifier, field, relationship as LegacyHasManyEdge | LegacyBelongsToEdge); changed.push(field); } } @@ -425,7 +497,7 @@ export class Graph { ): void { assert( `Cannot update an implicit relationship`, - op.op === 'deleteRecord' || op.op === 'mergeIdentifiers' || !isImplicit(this.get(op.record, op.field)) + op.op === 'deleteRecord' || op.op === 'mergeIdentifiers' || !isImplicitEdge(this.get(op.record, op.field)) ); if (LOG_GRAPH) { // eslint-disable-next-line no-console @@ -490,7 +562,7 @@ export class Graph { } } - _scheduleLocalSync(relationship: CollectionEdge) { + _scheduleLocalSync(relationship: LegacyHasManyEdge) { this._updatedRelationships.add(relationship); if (!this._willSyncLocal) { this._willSyncLocal = true; @@ -511,20 +583,20 @@ export class Graph { setTransient('transactionRef', transactionRef); this._willSyncRemote = false; const updates = this._pushedUpdates; - const { deletions, hasMany, belongsTo } = updates; + const { deletions, multi, single } = updates; updates.deletions = []; - updates.hasMany = undefined; - updates.belongsTo = undefined; + updates.multi = undefined; + updates.single = undefined; for (let i = 0; i < deletions.length; i++) { this.update(deletions[i], true); } - if (hasMany) { - flushPending(this, hasMany); + if (multi) { + flushPending(this, multi); } - if (belongsTo) { - flushPending(this, belongsTo); + if (single) { + flushPending(this, single); } this._transaction = null; @@ -536,7 +608,7 @@ export class Graph { } } - _addToTransaction(relationship: CollectionEdge | ResourceEdge) { + _addToTransaction(relationship: LegacyHasManyEdge | LegacyBelongsToEdge) { assert(`expected a transaction`, this._transaction !== null); if (LOG_GRAPH) { // eslint-disable-next-line no-console @@ -606,7 +678,7 @@ function flushPendingList(graph: Graph, opList: RemoteRelationshipOperation[]) { // disconnect the graph. Because it's not async, we don't need to keep around // the identifier as an id-wrapper for references function destroyRelationship(graph: Graph, rel: GraphEdge, silenceNotifications?: boolean) { - if (isImplicit(rel)) { + if (isImplicitEdge(rel)) { if (graph.isReleasable(rel.identifier)) { /*#__NOINLINE__*/ removeCompletelyFromInverse(graph, rel); } @@ -658,37 +730,39 @@ function notifyInverseOfDematerialization( } const relationship = graph.get(inverseIdentifier, inverseKey); - assert(`expected no implicit`, !isImplicit(relationship)); + assert(`expected no implicit`, !isImplicitEdge(relationship)); // For remote members, it is possible that inverseRecordData has already been associated to // to another record. For such cases, do not dematerialize the inverseRecordData - if (!isBelongsTo(relationship) || !relationship.localState || identifier === relationship.localState) { + if (!isSingleEdge(relationship) || !relationship.localState || identifier === relationship.localState) { /*#__NOINLINE__*/ removeDematerializedInverse(graph, relationship, identifier, silenceNotifications); } } -function clearRelationship(relationship: CollectionEdge | ResourceEdge) { - if (isBelongsTo(relationship)) { +function clearRelationship(relationship: LegacyHasManyEdge | LegacyBelongsToEdge | ResourceEdge | CollectionEdge) { + if (isBelongsToEdge(relationship)) { relationship.localState = null; relationship.remoteState = null; relationship.state.hasReceivedData = false; relationship.state.isEmpty = true; - } else { + } else if (isHasManyEdge(relationship)) { relationship.remoteMembers.clear(); relationship.remoteState = []; relationship.additions = null; relationship.removals = null; relationship.localState = null; + } else { + throw new Error('not implemented'); } } function removeDematerializedInverse( graph: Graph, - relationship: CollectionEdge | ResourceEdge, + relationship: LegacyHasManyEdge | LegacyBelongsToEdge | ResourceEdge | CollectionEdge, inverseIdentifier: StableRecordIdentifier, silenceNotifications?: boolean ) { - if (isBelongsTo(relationship)) { + if (isBelongsToEdge(relationship)) { const localInverse = relationship.localState; if (!relationship.definition.isAsync || (localInverse && isNew(localInverse))) { // unloading inverse of a sync relationship is treated as a client-side @@ -715,7 +789,7 @@ function removeDematerializedInverse( if (!silenceNotifications) { notifyChange(graph, relationship.identifier, relationship.definition.key); } - } else { + } else if (isHasManyEdge(relationship)) { if (!relationship.definition.isAsync || (inverseIdentifier && isNew(inverseIdentifier))) { // unloading inverse of a sync relationship is treated as a client-side // delete, so actually remove the models don't merely invalidate the cp @@ -730,6 +804,8 @@ function removeDematerializedInverse( if (!silenceNotifications) { notifyChange(graph, relationship.identifier, relationship.definition.key); } + } else { + throw new Error('not implemented'); } } @@ -743,21 +819,23 @@ function removeCompletelyFromInverse(graph: Graph, relationship: GraphEdge) { } }); - if (isBelongsTo(relationship)) { + if (isBelongsToEdge(relationship)) { if (!relationship.definition.isAsync) { clearRelationship(relationship); } relationship.localState = null; - } else if (isHasMany(relationship)) { + } else if (isHasManyEdge(relationship)) { if (!relationship.definition.isAsync) { clearRelationship(relationship); notifyChange(graph, relationship.identifier, relationship.definition.key); } - } else { + } else if (isImplicitEdge(relationship)) { relationship.remoteMembers.clear(); relationship.localMembers.clear(); + } else { + throw new Error('not implemented'); } } @@ -766,8 +844,8 @@ function addPending( definition: UpgradedMeta, op: RemoteRelationshipOperation & { field: string } ): void { - const lc = (cache[definition.kind as 'hasMany' | 'belongsTo'] = - cache[definition.kind as 'hasMany' | 'belongsTo'] || new Map>()); + const updateKind = updateTypeForKind(definition.kind); + const lc = (cache[updateKind] = cache[updateKind] || new Map>()); let lc2 = lc.get(definition.inverseType); if (!lc2) { lc2 = new Map(); @@ -781,7 +859,7 @@ function addPending( arr.push(op); } -function isReordered(relationship: CollectionEdge): boolean { +function isReordered(relationship: LegacyHasManyEdge): boolean { // if we are dirty we are never re-ordered because accessing // the state would flush away any reordering. if (relationship.isDirty) { diff --git a/packages/graph/src/-private/operations/add-to-related-records.ts b/packages/graph/src/-private/operations/add-to-related-records.ts index 2c08cb4981b..195c4e2db0f 100644 --- a/packages/graph/src/-private/operations/add-to-related-records.ts +++ b/packages/graph/src/-private/operations/add-to-related-records.ts @@ -3,8 +3,8 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { AddToRelatedRecordsOperation } from '@warp-drive/core-types/graph'; import { _addLocal } from '../-diff'; -import { isHasMany, notifyChange } from '../-utils'; -import type { CollectionEdge } from '../edges/collection'; +import { isHasManyEdge, notifyChange } from '../-utils'; +import type { LegacyHasManyEdge } from '../edges/has-many'; import type { Graph } from '../graph'; import { addToInverse } from './replace-related-records'; @@ -17,7 +17,7 @@ export default function addToRelatedRecords(graph: Graph, op: AddToRelatedRecord const relationship = graph.get(record, op.field); assert( `You can only '${op.op}' on a hasMany relationship. ${record.type}.${op.field} is a ${relationship.definition.kind}`, - isHasMany(relationship) + isHasManyEdge(relationship) ); if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { @@ -32,7 +32,7 @@ export default function addToRelatedRecords(graph: Graph, op: AddToRelatedRecord function addRelatedRecord( graph: Graph, - relationship: CollectionEdge, + relationship: LegacyHasManyEdge, record: StableRecordIdentifier, value: StableRecordIdentifier, index: number | undefined, diff --git a/packages/graph/src/-private/operations/merge-identifier.ts b/packages/graph/src/-private/operations/merge-identifier.ts index 8c95f40179b..3d1b1f955ea 100644 --- a/packages/graph/src/-private/operations/merge-identifier.ts +++ b/packages/graph/src/-private/operations/merge-identifier.ts @@ -1,9 +1,9 @@ import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; -import { forAllRelatedIdentifiers, isBelongsTo, isHasMany, notifyChange } from '../-utils'; -import type { CollectionEdge } from '../edges/collection'; +import { forAllRelatedIdentifiers, isBelongsToEdge, isHasManyEdge, notifyChange } from '../-utils'; +import type { LegacyHasManyEdge } from '../edges/has-many'; import type { ImplicitEdge } from '../edges/implicit'; -import type { ResourceEdge } from '../edges/resource'; +import type { LegacyBelongsToEdge } from '../edges/belongs-to'; import type { Graph, GraphEdge } from '../graph'; export function mergeIdentifier(graph: Graph, op: MergeOperation, relationships: Record) { @@ -25,16 +25,16 @@ function mergeIdentifierForRelationship(graph: Graph, op: MergeOperation, rel: G } function mergeInRelationship(graph: Graph, rel: GraphEdge, op: MergeOperation): void { - if (isBelongsTo(rel)) { + if (isBelongsToEdge(rel)) { mergeBelongsTo(graph, rel, op); - } else if (isHasMany(rel)) { + } else if (isHasManyEdge(rel)) { mergeHasMany(graph, rel, op); } else { mergeImplicit(graph, rel, op); } } -function mergeBelongsTo(graph: Graph, rel: ResourceEdge, op: MergeOperation): void { +function mergeBelongsTo(graph: Graph, rel: LegacyBelongsToEdge, op: MergeOperation): void { if (rel.remoteState === op.record) { rel.remoteState = op.value; } @@ -44,7 +44,7 @@ function mergeBelongsTo(graph: Graph, rel: ResourceEdge, op: MergeOperation): vo } } -function mergeHasMany(graph: Graph, rel: CollectionEdge, op: MergeOperation): void { +function mergeHasMany(graph: Graph, rel: LegacyHasManyEdge, op: MergeOperation): void { if (rel.remoteMembers.has(op.record)) { rel.remoteMembers.delete(op.record); rel.remoteMembers.add(op.value); diff --git a/packages/graph/src/-private/operations/remove-from-related-records.ts b/packages/graph/src/-private/operations/remove-from-related-records.ts index 63f198d136f..15dc17fbccf 100644 --- a/packages/graph/src/-private/operations/remove-from-related-records.ts +++ b/packages/graph/src/-private/operations/remove-from-related-records.ts @@ -3,8 +3,8 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { RemoveFromRelatedRecordsOperation } from '@warp-drive/core-types/graph'; import { _removeLocal } from '../-diff'; -import { isHasMany, notifyChange } from '../-utils'; -import type { CollectionEdge } from '../edges/collection'; +import { isHasManyEdge, notifyChange } from '../-utils'; +import type { LegacyHasManyEdge } from '../edges/has-many'; import type { Graph } from '../graph'; import { removeFromInverse } from './replace-related-records'; @@ -17,7 +17,7 @@ export default function removeFromRelatedRecords(graph: Graph, op: RemoveFromRel const relationship = graph.get(record, op.field); assert( `You can only '${op.op}' on a hasMany relationship. ${record.type}.${op.field} is a ${relationship.definition.kind}`, - isHasMany(relationship) + isHasManyEdge(relationship) ); // TODO we should potentially thread the index information through here // when available as it may make it faster to remove from the local state @@ -35,7 +35,7 @@ export default function removeFromRelatedRecords(graph: Graph, op: RemoveFromRel function removeRelatedRecord( graph: Graph, - relationship: CollectionEdge, + relationship: LegacyHasManyEdge, record: StableRecordIdentifier, value: StableRecordIdentifier, isRemote: false diff --git a/packages/graph/src/-private/operations/replace-related-record.ts b/packages/graph/src/-private/operations/replace-related-record.ts index 5779fc2edea..74b5723c57b 100644 --- a/packages/graph/src/-private/operations/replace-related-record.ts +++ b/packages/graph/src/-private/operations/replace-related-record.ts @@ -6,7 +6,7 @@ import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { ReplaceRelatedRecordOperation } from '@warp-drive/core-types/graph'; -import { isBelongsTo, isNew, notifyChange } from '../-utils'; +import { isBelongsToEdge, isNew, notifyChange } from '../-utils'; import { assertPolymorphicType } from '../debug/assert-polymorphic-type'; import type { Graph } from '../graph'; import { addToInverse, notifyInverseOfPotentialMaterialization, removeFromInverse } from './replace-related-records'; @@ -15,7 +15,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec const relationship = graph.get(op.record, op.field); assert( `You can only '${op.op}' on a belongsTo relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`, - isBelongsTo(relationship) + isBelongsToEdge(relationship) ); if (isRemote) { graph._addToTransaction(relationship); diff --git a/packages/graph/src/-private/operations/replace-related-records.ts b/packages/graph/src/-private/operations/replace-related-records.ts index f0dfdc07acb..2c70ac137d1 100644 --- a/packages/graph/src/-private/operations/replace-related-records.ts +++ b/packages/graph/src/-private/operations/replace-related-records.ts @@ -7,9 +7,9 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { ReplaceRelatedRecordsOperation } from '@warp-drive/core-types/graph'; import { _addLocal, _removeLocal, _removeRemote, diffCollection } from '../-diff'; -import { isBelongsTo, isHasMany, isNew, notifyChange } from '../-utils'; +import { isBelongsToEdge, isHasManyEdge, isNew, notifyChange } from '../-utils'; import { assertPolymorphicType } from '../debug/assert-polymorphic-type'; -import type { CollectionEdge } from '../edges/collection'; +import type { LegacyHasManyEdge } from '../edges/has-many'; import type { Graph } from '../graph'; /* @@ -78,7 +78,7 @@ export default function replaceRelatedRecords(graph: Graph, op: ReplaceRelatedRe function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { const identifiers = op.value; const relationship = graph.get(op.record, op.field); - assert(`expected hasMany relationship`, isHasMany(relationship)); + assert(`expected hasMany relationship`, isHasManyEdge(relationship)); // relationships for newly created records begin in the dirty state, so if updated // before flushed we would fail to notify. This check helps us avoid that. @@ -173,7 +173,7 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper assert( `You can only '${op.op}' on a hasMany relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`, - isHasMany(relationship) + isHasManyEdge(relationship) ); if (isRemote) { graph._addToTransaction(relationship); @@ -322,7 +322,7 @@ export function addToInverse( graph.registerPolymorphicType(type, value.type); } - if (isBelongsTo(relationship)) { + if (isBelongsToEdge(relationship)) { relationship.state.hasReceivedData = true; relationship.state.isEmpty = false; @@ -341,7 +341,7 @@ export function addToInverse( relationship.localState = value; notifyChange(graph, identifier, key); } - } else if (isHasMany(relationship)) { + } else if (isHasManyEdge(relationship)) { if (isRemote) { // TODO this needs to alert stuffs // And patch state better @@ -387,7 +387,7 @@ export function notifyInverseOfPotentialMaterialization( isRemote: boolean ) { const relationship = graph.get(identifier, key); - if (isHasMany(relationship) && isRemote && relationship.remoteMembers.has(value)) { + if (isHasManyEdge(relationship) && isRemote && relationship.remoteMembers.has(value)) { notifyChange(graph, identifier, key); } } @@ -401,7 +401,7 @@ export function removeFromInverse( ) { const relationship = graph.get(identifier, key); - if (isBelongsTo(relationship)) { + if (isBelongsToEdge(relationship)) { relationship.state.isEmpty = true; if (isRemote) { graph._addToTransaction(relationship); @@ -412,7 +412,7 @@ export function removeFromInverse( notifyChange(graph, identifier, key); } - } else if (isHasMany(relationship)) { + } else if (isHasManyEdge(relationship)) { if (isRemote) { graph._addToTransaction(relationship); if (_removeRemote(relationship, value)) { @@ -435,6 +435,6 @@ export function removeFromInverse( } } -function flushCanonical(graph: Graph, rel: CollectionEdge) { +function flushCanonical(graph: Graph, rel: LegacyHasManyEdge) { graph._scheduleLocalSync(rel); } diff --git a/packages/graph/src/-private/operations/update-relationship.ts b/packages/graph/src/-private/operations/update-relationship.ts index 96fd82f7887..c4d23b83417 100644 --- a/packages/graph/src/-private/operations/update-relationship.ts +++ b/packages/graph/src/-private/operations/update-relationship.ts @@ -9,19 +9,44 @@ import type { NewResourceIdentifierObject, } from '@warp-drive/core-types/spec/json-api-raw'; -import { isBelongsTo, isHasMany, notifyChange } from '../-utils'; +import { isCollectionEdge, isImplicitEdge, isResourceEdge, notifyChange } from '../-utils'; +import type { LegacyBelongsToEdge } from '../edges/belongs-to'; +import type { CollectionEdge } from '../edges/collection'; +import type { LegacyHasManyEdge } from '../edges/has-many'; +import type { ResourceEdge } from '../edges/resource'; import type { Graph } from '../graph'; import _normalizeLink from '../normalize-link'; type IdentifierCache = Store['identifierCache']; +export function updateRelationshipOperation(graph: Graph, op: UpdateRelationshipOperation) { + const relationship = graph.get(op.record, op.field); + assert(`Cannot update an implicit relationship`, !isImplicitEdge(relationship)); + + if (isResourceEdge(relationship) || isCollectionEdge(relationship)) { + updateModernRelationshipOperation(graph, op, relationship); + } else { + legacyUpdateRelationshipOperation(graph, op, relationship); + } +} + +function updateModernRelationshipOperation( + graph: Graph, + op: UpdateRelationshipOperation, + relationship: ResourceEdge | CollectionEdge +) { + throw new Error('Not implemented'); +} + /* Updates the "canonical" or "remote" state of a relationship, replacing any existing state and blowing away any local changes (excepting new records). */ -export default function updateRelationshipOperation(graph: Graph, op: UpdateRelationshipOperation) { - const relationship = graph.get(op.record, op.field); - assert(`Cannot update an implicit relationship`, isHasMany(relationship) || isBelongsTo(relationship)); +function legacyUpdateRelationshipOperation( + graph: Graph, + op: UpdateRelationshipOperation, + relationship: LegacyBelongsToEdge | LegacyHasManyEdge +) { const { definition, state, identifier } = relationship; const { isCollection } = definition; diff --git a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts index d1618279415..ac290ae8cec 100644 --- a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts +++ b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts @@ -183,6 +183,10 @@ function ensureRelationshipIsSetToParent( if (inverse) { const { inverseKey, kind } = inverse; + if (kind === 'collection') { + return; + } + const relationshipData = relationships[inverseKey]?.data as RelationshipData | undefined; if (DEBUG) { diff --git a/packages/model/src/-private/debug/assert-polymorphic-type.ts b/packages/model/src/-private/debug/assert-polymorphic-type.ts index 49c451e0f99..0426dde5b3b 100644 --- a/packages/model/src/-private/debug/assert-polymorphic-type.ts +++ b/packages/model/src/-private/debug/assert-polymorphic-type.ts @@ -42,7 +42,7 @@ if (DEBUG) { ); assert( `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. The definition should specify 'as: "${parentDefinition.type}"' in options.`, - meta?.options.as === parentDefinition.type + meta?.options?.as === parentDefinition.type ); } }; diff --git a/packages/store/src/-types/q/ds-model.ts b/packages/store/src/-types/q/ds-model.ts index 28df41767c6..6aaf89b7df1 100644 --- a/packages/store/src/-types/q/ds-model.ts +++ b/packages/store/src/-types/q/ds-model.ts @@ -2,6 +2,7 @@ import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-typ import type { LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; export type KeyOrString = keyof T & string extends never ? string : keyof T & string; +export type FieldKind = 'attribute' | 'belongsTo' | 'hasMany' | 'collection'; /** * Minimum subset of static schema methods and properties on the diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08740573a1d..16a77150c9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2487,6 +2487,9 @@ importers: '@warp-drive/internal-config': specifier: workspace:5.4.0-alpha.97 version: link:../../config + '@warp-drive/schema-record': + specifier: workspace:0.0.0-alpha.83 + version: file:packages/schema-record(@babel/core@7.24.5)(@ember-data/model@5.4.0-alpha.97)(@ember-data/request@5.4.0-alpha.97)(@ember-data/store@5.4.0-alpha.97)(@ember-data/tracking@5.4.0-alpha.97)(@warp-drive/core-types@0.0.0-alpha.83) ember-auto-import: specifier: ^2.7.4 version: 2.7.4(@glint/template@1.4.0) @@ -2568,6 +2571,8 @@ importers: injected: true '@warp-drive/diagnostic': injected: true + '@warp-drive/schema-record': + injected: true tests/ember-data__json-api: dependencies: diff --git a/tests/ember-data__graph/config/environment.js b/tests/ember-data__graph/config/environment.js index 94e67a34e77..78ae1c521a6 100644 --- a/tests/ember-data__graph/config/environment.js +++ b/tests/ember-data__graph/config/environment.js @@ -6,16 +6,7 @@ module.exports = function (environment) { environment, rootURL: '/', locationType: 'history', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, + EmberENV: {}, APP: { // Here you can pass flags/options to your application instance diff --git a/tests/ember-data__graph/package.json b/tests/ember-data__graph/package.json index 66faecd029e..8233782471d 100644 --- a/tests/ember-data__graph/package.json +++ b/tests/ember-data__graph/package.json @@ -20,6 +20,7 @@ "lint": "eslint . --quiet --cache --cache-strategy=content --report-unused-disable-directives", "check:types": "tsc --noEmit", "test": "bun ./diagnostic.js", + "start": "pnpm build:tests --watch", "test:production": "bun ./diagnostic.js", "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, @@ -62,6 +63,9 @@ }, "@ember-data/debug": { "injected": true + }, + "@warp-drive/schema-record": { + "injected": true } }, "devDependencies": { @@ -88,6 +92,7 @@ "@warp-drive/build-config": "workspace:0.0.0-alpha.34", "@warp-drive/core-types": "workspace:0.0.0-alpha.83", "@warp-drive/internal-config": "workspace:5.4.0-alpha.97", + "@warp-drive/schema-record": "workspace:0.0.0-alpha.83", "@warp-drive/diagnostic": "workspace:0.0.0-alpha.83", "ember-auto-import": "^2.7.4", "ember-cli": "~5.9.0", @@ -121,4 +126,4 @@ "dependencies": { "pnpm-sync-dependencies-meta-injected": "0.0.14" } -} +} \ No newline at end of file diff --git a/tests/ember-data__graph/tests/integration/edge/resource-to-none-test.ts b/tests/ember-data__graph/tests/integration/edge/resource-to-none-test.ts new file mode 100644 index 00000000000..73b8e2dbf41 --- /dev/null +++ b/tests/ember-data__graph/tests/integration/edge/resource-to-none-test.ts @@ -0,0 +1,97 @@ +import type { Graph } from '@ember-data/graph/-private'; +import { graphFor } from '@ember-data/graph/-private'; +import Store from '@ember-data/store'; +import type { StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier'; +import { module, test as _test } from '@warp-drive/diagnostic'; +import type { TestContext } from '@warp-drive/diagnostic/ember'; +import { registerDerivations, SchemaService, withDefaults } from '@warp-drive/schema-record/schema'; + +interface LocalTestContext extends TestContext { + store: TestStore; + graph: Graph; + ref(type: string, id: string): StableExistingRecordIdentifier; + run(callback: () => T): T; +} + +type DiagnosticTest = Parameters>[1]; +function test(name: string, callback: DiagnosticTest): void { + return _test(name, callback); +} + +class TestStore extends Store { + createSchemaService() { + const schema = new SchemaService(); + registerDerivations(schema); + return schema; + } +} + +module('Integration | Graph | Edge > resource-to-none', function (hooks) { + hooks.beforeEach(function () { + this.store = new TestStore(); + this.graph = graphFor(this.store._instanceCache._storeWrapper); + this.ref = (type: string, id: string) => + this.store.identifierCache.getOrCreateRecordIdentifier({ type, id }) as StableExistingRecordIdentifier; + this.run = (callback: () => T): T => { + let result: T; + this.store._run(() => { + result = callback(); + }); + return result!; + }; + }); + + test('we can insert a resource edge and then retrieve it', function (assert) { + const { store, graph } = this; + + // create a schema for a has-none relationship + const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + }, + ], + }); + store.schema.registerResource(UserSchema); + + // generate a few nodes + const user1 = this.ref('user', '1'); + const user2 = this.ref('user', '2'); + + // insert a bestFriend payload + this.run(() => + graph.push({ + op: 'updateRelationship', + record: user1, + field: 'bestFriend', + value: { + links: { + related: '/users/1/bestFriend', + self: '/users/1/relationships/bestFriend', + }, + meta: {}, + data: user2, + }, + }) + ); + + // retrieve the edge + const edge = graph.getData(user1, 'bestFriend'); + + assert.deepEqual( + edge, + { + links: { + related: '/users/1/bestFriend', + self: '/users/1/relationships/bestFriend', + }, + meta: {}, + data: user2, + }, + 'we can retrieve the edge' + ); + }); +}); diff --git a/tests/ember-data__graph/tests/integration/graph.ts b/tests/ember-data__graph/tests/integration/graph.ts deleted file mode 100644 index 526d87cf670..00000000000 --- a/tests/ember-data__graph/tests/integration/graph.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { module, test } from '@warp-drive/diagnostic'; - -module('RecordData', function () { - test('Test Suit Configured', function (assert) { - assert.ok('We are configured'); - }); -}); diff --git a/tests/ember-data__graph/tests/integration/schema/resource-to-none-test.ts b/tests/ember-data__graph/tests/integration/schema/resource-to-none-test.ts new file mode 100644 index 00000000000..2c8244ec294 --- /dev/null +++ b/tests/ember-data__graph/tests/integration/schema/resource-to-none-test.ts @@ -0,0 +1,447 @@ +import type { Graph } from '@ember-data/graph/-private'; +import { graphFor } from '@ember-data/graph/-private'; +import type { EdgeDefinition } from '@ember-data/graph/-private/-edge-definition'; +import Store from '@ember-data/store'; +import { module, test as _test } from '@warp-drive/diagnostic'; +import type { TestContext } from '@warp-drive/diagnostic/ember'; +import { registerDerivations, SchemaService, withDefaults } from '@warp-drive/schema-record/schema'; + +interface LocalTestContext extends TestContext { + store: TestStore; + graph: Graph; +} + +type DiagnosticTest = Parameters>[1]; +function test(name: string, callback: DiagnosticTest): void { + return _test(name, callback); +} + +class TestStore extends Store { + createSchemaService() { + const schema = new SchemaService(); + registerDerivations(schema); + return schema; + } +} + +module('Integration | Graph | Schema > resource-to-none', function (hooks) { + hooks.beforeEach(function () { + this.store = new TestStore(); + this.graph = graphFor(this.store._instanceCache._storeWrapper); + }); + + test('Graph.getDefinition works as expected', function (assert) { + const { store, graph } = this; + + // create a schema for a has-none relationship + const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + }, + ], + }); + store.schema.registerResource(UserSchema); + + // check that we can call getDefinition and get back something meaningful + const userDefinition = graph.getDefinition({ type: 'user' }, 'bestFriend'); + const implicitKey = userDefinition?.inverseKey; + assert.true(implicitKey?.startsWith('implicit-user:bestFriend'), 'implicit key is generated correctly'); + assert.notEqual(implicitKey, 'implicit-user:bestFriend', 'implicit key is not just the prefix'); + + const expected = { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: implicitKey, + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: true, + inverseIsCollection: true, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'implicit', + inverseIsPolymorphic: false, + }; + + assert.deepEqual(userDefinition, expected, 'getDefinition returns the expected definition'); + + const actualEdge = graph._definitionCache.user?.bestFriend; + const expectedEdge: EdgeDefinition = { + lhs_key: 'user:bestFriend', + lhs_modelNames: ['user'], + lhs_baseModelName: 'user', + lhs_relationshipName: 'bestFriend', + lhs_definition: { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: implicitKey, + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: true, + inverseIsCollection: true, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'implicit', + inverseIsPolymorphic: false, + }, + lhs_isPolymorphic: false, + rhs_key: implicitKey, + rhs_modelNames: ['user'], + rhs_baseModelName: 'user', + rhs_relationshipName: implicitKey, + rhs_definition: { + kind: 'implicit', + key: implicitKey, + type: 'user', + isAsync: false, + isImplicit: true, + isCollection: true, + isPaginated: false, + isPolymorphic: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsCollection: false, + inverseIsPaginated: false, + inverseIsPolymorphic: false, + inverseIsImplicit: false, + }, + rhs_isPolymorphic: false, + hasInverse: false, + isSelfReferential: true, + isReflexive: false, + }; + + assert.deepEqual(actualEdge, expectedEdge, 'edge is created correctly'); + }); + + test('Graph.getDefinition works as expected with explicit inverse: null', function (assert) { + const { store, graph } = this; + + // create a schema for a has-none relationship + const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + options: { + inverse: null, + }, + }, + ], + }); + store.schema.registerResource(UserSchema); + + // check that we can call getDefinition and get back something meaningful + const userDefinition = graph.getDefinition({ type: 'user' }, 'bestFriend'); + const implicitKey = userDefinition?.inverseKey; + assert.true(implicitKey?.startsWith('implicit-user:bestFriend'), 'implicit key is generated correctly'); + assert.notEqual(implicitKey, 'implicit-user:bestFriend', 'implicit key is not just the prefix'); + + const expected = { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: implicitKey, + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: true, + inverseIsCollection: true, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'implicit', + inverseIsPolymorphic: false, + }; + + assert.deepEqual(userDefinition, expected, 'getDefinition returns the expected definition'); + + const actualEdge = graph._definitionCache.user?.bestFriend; + const expectedEdge: EdgeDefinition = { + lhs_key: 'user:bestFriend', + lhs_modelNames: ['user'], + lhs_baseModelName: 'user', + lhs_relationshipName: 'bestFriend', + lhs_definition: { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: implicitKey, + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: true, + inverseIsCollection: true, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'implicit', + inverseIsPolymorphic: false, + }, + lhs_isPolymorphic: false, + rhs_key: implicitKey, + rhs_modelNames: ['user'], + rhs_baseModelName: 'user', + rhs_relationshipName: implicitKey, + rhs_definition: { + kind: 'implicit', + key: implicitKey, + type: 'user', + isAsync: false, + isImplicit: true, + isCollection: true, + isPaginated: false, + isPolymorphic: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsCollection: false, + inverseIsPaginated: false, + inverseIsPolymorphic: false, + inverseIsImplicit: false, + }, + rhs_isPolymorphic: false, + hasInverse: false, + isSelfReferential: true, + isReflexive: false, + }; + + assert.deepEqual(actualEdge, expectedEdge, 'edge is created correctly'); + }); + + test('Graph.getDefinition works as expected with async: true', function (assert) { + const { store, graph } = this; + + // create a schema for a has-none relationship + const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + options: { async: true }, + }, + ], + }); + store.schema.registerResource(UserSchema); + + // check that we can call getDefinition and get back something meaningful + const userDefinition = graph.getDefinition({ type: 'user' }, 'bestFriend'); + const implicitKey = userDefinition?.inverseKey; + assert.true(implicitKey?.startsWith('implicit-user:bestFriend'), 'implicit key is generated correctly'); + assert.notEqual(implicitKey, 'implicit-user:bestFriend', 'implicit key is not just the prefix'); + + const expected = { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: true, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: implicitKey, + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: true, + inverseIsCollection: true, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'implicit', + inverseIsPolymorphic: false, + }; + + assert.deepEqual(userDefinition, expected, 'getDefinition returns the expected definition'); + + const actualEdge = graph._definitionCache.user?.bestFriend; + const expectedEdge: EdgeDefinition = { + lhs_key: 'user:bestFriend', + lhs_modelNames: ['user'], + lhs_baseModelName: 'user', + lhs_relationshipName: 'bestFriend', + lhs_definition: { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: true, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: implicitKey, + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: true, + inverseIsCollection: true, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'implicit', + inverseIsPolymorphic: false, + }, + lhs_isPolymorphic: false, + rhs_key: implicitKey, + rhs_modelNames: ['user'], + rhs_baseModelName: 'user', + rhs_relationshipName: implicitKey, + rhs_definition: { + kind: 'implicit', + key: implicitKey, + type: 'user', + isAsync: false, + isImplicit: true, + isCollection: true, + isPaginated: false, + isPolymorphic: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: true, + inverseIsCollection: false, + inverseIsPaginated: false, + inverseIsPolymorphic: false, + inverseIsImplicit: false, + }, + rhs_isPolymorphic: false, + hasInverse: false, + isSelfReferential: true, + isReflexive: false, + }; + + assert.deepEqual(actualEdge, expectedEdge, 'edge is created correctly'); + }); + + test('Graph.getDefinition works as expected with polymorphics', function (assert) { + const { store, graph } = this; + + // create a schema for a has-none relationship + const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + options: { + polymorphic: true, + }, + }, + ], + }); + store.schema.registerResource(UserSchema); + + // check that we can call getDefinition and get back something meaningful + const userDefinition = graph.getDefinition({ type: 'user' }, 'bestFriend'); + const implicitKey = userDefinition?.inverseKey; + assert.true(implicitKey?.startsWith('implicit-user:bestFriend'), 'implicit key is generated correctly'); + assert.notEqual(implicitKey, 'implicit-user:bestFriend', 'implicit key is not just the prefix'); + + const expected = { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: true, + isPaginated: false, + inverseKey: implicitKey, + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: true, + inverseIsCollection: true, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'implicit', + inverseIsPolymorphic: false, + }; + + assert.deepEqual(userDefinition, expected, 'getDefinition returns the expected definition'); + + const actualEdge = graph._definitionCache.user?.bestFriend; + const expectedEdge: EdgeDefinition = { + lhs_key: 'user:bestFriend', + lhs_modelNames: ['user'], + lhs_baseModelName: 'user', + lhs_relationshipName: 'bestFriend', + lhs_definition: { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: true, + isPaginated: false, + inverseKey: implicitKey, + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: true, + inverseIsCollection: true, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'implicit', + inverseIsPolymorphic: false, + }, + lhs_isPolymorphic: true, + rhs_key: implicitKey, + rhs_modelNames: ['user'], + rhs_baseModelName: 'user', + rhs_relationshipName: implicitKey, + rhs_definition: { + kind: 'implicit', + key: implicitKey, + type: 'user', + isAsync: false, + isImplicit: true, + isCollection: true, + isPaginated: false, + isPolymorphic: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsCollection: false, + inverseIsPaginated: false, + inverseIsPolymorphic: true, + inverseIsImplicit: false, + }, + rhs_isPolymorphic: false, + hasInverse: false, + isSelfReferential: true, + isReflexive: false, + }; + + assert.deepEqual(actualEdge, expectedEdge, 'edge is created correctly'); + }); +}); diff --git a/tests/ember-data__graph/tests/integration/schema/resource-to-one-test.ts b/tests/ember-data__graph/tests/integration/schema/resource-to-one-test.ts new file mode 100644 index 00000000000..17ff19a6c94 --- /dev/null +++ b/tests/ember-data__graph/tests/integration/schema/resource-to-one-test.ts @@ -0,0 +1,462 @@ +import type { Graph } from '@ember-data/graph/-private'; +import { graphFor } from '@ember-data/graph/-private'; +import type { EdgeDefinition } from '@ember-data/graph/-private/-edge-definition'; +import Store from '@ember-data/store'; +import { module, test as _test } from '@warp-drive/diagnostic'; +import type { TestContext } from '@warp-drive/diagnostic/ember'; +import { registerDerivations, SchemaService, withDefaults } from '@warp-drive/schema-record/schema'; + +interface LocalTestContext extends TestContext { + store: TestStore; + graph: Graph; +} + +type DiagnosticTest = Parameters>[1]; +function test(name: string, callback: DiagnosticTest): void { + return _test(name, callback); +} + +class TestStore extends Store { + createSchemaService() { + const schema = new SchemaService(); + registerDerivations(schema); + return schema; + } +} + +module('Integration | Graph | Schema | resource-to-one', function (hooks) { + hooks.beforeEach(function () { + this.store = new TestStore(); + this.graph = graphFor(this.store._instanceCache._storeWrapper); + }); + + test('Graph.getDefinition works as expected', function (assert) { + const { store, graph } = this; + + // create a schema for a has-none relationship + const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'favoritePet', + kind: 'resource', + type: 'pet', + options: { inverse: 'bestFriend' }, + }, + ], + }); + const PetSchema = withDefaults({ + type: 'pet', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + options: { inverse: 'favoritePet' }, + }, + ], + }); + store.schema.registerResources([PetSchema, UserSchema]); + + // check that we can call getDefinition and get back something meaningful + const userDefinition = graph.getDefinition({ type: 'user' }, 'favoritePet'); + + const expected = { + kind: 'resource', + key: 'favoritePet', + type: 'pet', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: false, + inverseIsCollection: false, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseIsPolymorphic: false, + }; + + assert.deepEqual(userDefinition, expected, 'getDefinition returns the expected definition'); + + const actualEdge = graph._definitionCache.user?.favoritePet; + const expectedEdge: EdgeDefinition = { + lhs_key: 'user:favoritePet', + lhs_modelNames: ['user'], + lhs_baseModelName: 'user', + lhs_relationshipName: 'favoritePet', + lhs_definition: { + kind: 'resource', + key: 'favoritePet', + type: 'pet', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: false, + inverseIsCollection: false, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseIsPolymorphic: false, + }, + lhs_isPolymorphic: false, + rhs_key: 'pet:bestFriend', + rhs_modelNames: ['pet'], + rhs_baseModelName: 'pet', + rhs_relationshipName: 'bestFriend', + rhs_definition: { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPaginated: false, + isPolymorphic: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseKey: 'favoritePet', + inverseType: 'pet', + inverseIsAsync: false, + inverseIsCollection: false, + inverseIsPaginated: false, + inverseIsPolymorphic: false, + inverseIsImplicit: false, + }, + rhs_isPolymorphic: false, + hasInverse: true, + isSelfReferential: false, + isReflexive: false, + }; + + assert.deepEqual(actualEdge, expectedEdge, 'edge is created correctly'); + }); + + test('Graph.getDefinition works as expected with async: true', function (assert) { + const { store, graph } = this; + + // create a schema for a has-none relationship + const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'favoritePet', + kind: 'resource', + type: 'pet', + options: { inverse: 'bestFriend', async: true }, + }, + ], + }); + const PetSchema = withDefaults({ + type: 'pet', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + options: { inverse: 'favoritePet' }, + }, + ], + }); + store.schema.registerResources([PetSchema, UserSchema]); + + // check that we can call getDefinition and get back something meaningful + const userDefinition = graph.getDefinition({ type: 'user' }, 'favoritePet'); + + const expected = { + kind: 'resource', + key: 'favoritePet', + type: 'pet', + isAsync: true, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: false, + inverseIsCollection: false, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseIsPolymorphic: false, + }; + + assert.deepEqual(userDefinition, expected, 'getDefinition returns the expected definition'); + + const actualEdge = graph._definitionCache.user?.favoritePet; + const expectedEdge: EdgeDefinition = { + lhs_key: 'user:favoritePet', + lhs_modelNames: ['user'], + lhs_baseModelName: 'user', + lhs_relationshipName: 'favoritePet', + lhs_definition: { + kind: 'resource', + key: 'favoritePet', + type: 'pet', + isAsync: true, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: false, + inverseIsCollection: false, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseIsPolymorphic: false, + }, + lhs_isPolymorphic: false, + rhs_key: 'pet:bestFriend', + rhs_modelNames: ['pet'], + rhs_baseModelName: 'pet', + rhs_relationshipName: 'bestFriend', + rhs_definition: { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPaginated: false, + isPolymorphic: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseKey: 'favoritePet', + inverseType: 'pet', + inverseIsAsync: true, + inverseIsCollection: false, + inverseIsPaginated: false, + inverseIsPolymorphic: false, + inverseIsImplicit: false, + }, + rhs_isPolymorphic: false, + hasInverse: true, + isSelfReferential: false, + isReflexive: false, + }; + + assert.deepEqual(actualEdge, expectedEdge, 'edge is created correctly'); + }); + + test('Graph.getDefinition works as expected with polymorphics', function (assert) { + const { store, graph } = this; + + // create a schema for a has-none relationship + const UserSchema = withDefaults({ + type: 'user', + fields: [ + { + name: 'favoritePet', + kind: 'resource', + type: 'pet', + options: { inverse: 'bestFriend', polymorphic: true }, + }, + ], + }); + const PetSchema = withDefaults({ + type: 'pet', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + options: { as: 'pet', inverse: 'favoritePet' }, + }, + ], + }); + const DogSchema = withDefaults({ + type: 'dog', + fields: [ + { + name: 'bestFriend', + kind: 'resource', + type: 'user', + options: { as: 'pet', inverse: 'favoritePet' }, + }, + ], + }); + store.schema.registerResources([PetSchema, DogSchema, UserSchema]); + + // check that we can call getDefinition and get back something meaningful + const userDefinition = graph.getDefinition({ type: 'user' }, 'favoritePet'); + + const expected = { + kind: 'resource', + key: 'favoritePet', + type: 'pet', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: true, + isPaginated: false, + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: false, + inverseIsCollection: false, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseIsPolymorphic: false, + }; + + assert.deepEqual(userDefinition, expected, 'getDefinition returns the expected definition'); + + const actualEdge = graph._definitionCache.user?.favoritePet; + const expectedEdge: EdgeDefinition = { + lhs_key: 'user:favoritePet', + lhs_modelNames: ['user'], + lhs_baseModelName: 'user', + lhs_relationshipName: 'favoritePet', + lhs_definition: { + kind: 'resource', + key: 'favoritePet', + type: 'pet', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: true, + isPaginated: false, + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: false, + inverseIsCollection: false, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseIsPolymorphic: false, + }, + lhs_isPolymorphic: true, + rhs_key: 'pet:bestFriend', + rhs_modelNames: ['pet'], + rhs_baseModelName: 'pet', + rhs_relationshipName: 'bestFriend', + rhs_definition: { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPaginated: false, + isPolymorphic: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseKey: 'favoritePet', + inverseType: 'pet', + inverseIsAsync: false, + inverseIsCollection: false, + inverseIsPaginated: false, + inverseIsPolymorphic: true, + inverseIsImplicit: false, + }, + rhs_isPolymorphic: false, + hasInverse: true, + isSelfReferential: false, + isReflexive: false, + }; + + assert.deepEqual(actualEdge, expectedEdge, 'edge is created correctly'); + + ////////////////////////////////////////// + // now get dog definition to fill out the polymorphic side + ////////////////////////////////////////// + + // check that we can call getDefinition and get back something meaningful + const dogDefinition = graph.getDefinition({ type: 'dog' }, 'bestFriend'); + + const finalExpected = { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: false, + isPaginated: false, + inverseKey: 'favoritePet', + inverseType: 'pet', + inverseIsAsync: false, + inverseIsImplicit: false, + inverseIsCollection: false, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseIsPolymorphic: true, + }; + + assert.deepEqual(dogDefinition, finalExpected, 'getDefinition returns the expected definition'); + + const finalActualEdge = graph._definitionCache.user?.favoritePet; + const finalExpectedEdge: EdgeDefinition = { + lhs_key: 'user:favoritePet', + lhs_modelNames: ['user'], + lhs_baseModelName: 'user', + lhs_relationshipName: 'favoritePet', + lhs_definition: { + kind: 'resource', + key: 'favoritePet', + type: 'pet', + isAsync: false, + isImplicit: false, + isCollection: false, + isPolymorphic: true, + isPaginated: false, + inverseKey: 'bestFriend', + inverseType: 'user', + inverseIsAsync: false, + inverseIsImplicit: false, + inverseIsCollection: false, + inverseIsPaginated: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseIsPolymorphic: false, + }, + lhs_isPolymorphic: true, + rhs_key: 'pet:bestFriend', + rhs_modelNames: ['pet', 'dog'], + rhs_baseModelName: 'pet', + rhs_relationshipName: 'bestFriend', + rhs_definition: { + kind: 'resource', + key: 'bestFriend', + type: 'user', + isAsync: false, + isImplicit: false, + isCollection: false, + isPaginated: false, + isPolymorphic: false, + resetOnRemoteUpdate: false, + inverseKind: 'resource', + inverseKey: 'favoritePet', + inverseType: 'pet', + inverseIsAsync: false, + inverseIsCollection: false, + inverseIsPaginated: false, + inverseIsPolymorphic: true, + inverseIsImplicit: false, + }, + rhs_isPolymorphic: false, + hasInverse: true, + isSelfReferential: false, + isReflexive: false, + }; + + assert.deepEqual(finalActualEdge, finalExpectedEdge, 'edge is created correctly'); + }); +}); diff --git a/tests/ember-data__graph/tests/test-helper.ts b/tests/ember-data__graph/tests/test-helper.ts index c380c8a49c8..e2eea27949a 100644 --- a/tests/ember-data__graph/tests/test-helper.ts +++ b/tests/ember-data__graph/tests/test-helper.ts @@ -16,9 +16,10 @@ configure(); setApplication(Application.create(config.APP)); void start({ - tryCatch: true, + tryCatch: false, groupLogs: false, instrument: true, - hideReport: true, + hideReport: false, useDiagnostic: true, + debug: true, }); diff --git a/tests/ember-data__graph/tsconfig.json b/tests/ember-data__graph/tsconfig.json index d299b914f1c..16d7e5352b6 100644 --- a/tests/ember-data__graph/tsconfig.json +++ b/tests/ember-data__graph/tsconfig.json @@ -47,7 +47,9 @@ "@warp-drive/core-types": ["../../packages/core-types/unstable-preview-types"], "@warp-drive/core-types/*": ["../../packages/core-types/unstable-preview-types/*"], "@warp-drive/diagnostic": ["../../packages/diagnostic/unstable-preview-types"], - "@warp-drive/diagnostic/*": ["../../packages/diagnostic/unstable-preview-types/*"] + "@warp-drive/diagnostic/*": ["../../packages/diagnostic/unstable-preview-types/*"], + "@warp-drive/schema-record": ["../../packages/schema-record/unstable-preview-types"], + "@warp-drive/schema-record/*": ["../../packages/schema-record/unstable-preview-types/*"] }, "types": ["ember-source/types"] }, @@ -88,6 +90,9 @@ { "path": "../../packages/core-types" }, + { + "path": "../../packages/schema-record" + }, { "path": "../../packages/diagnostic" }