UNPKG

@aws-amplify/datastore

Version:

AppSyncLocal support for aws-amplify

272 lines (269 loc) • 9.33 kB
import { isFieldAssociation } from '../types.mjs'; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 /** * Defines a relationship from a LOCAL model.field to a REMOTE model.field and helps * navigate the relationship, providing a simplified peek at the relationship details * pertinent to setting FK's and constructing join conditions. * * Because I mean, relationships are tough. * */ class ModelRelationship { /** * @param modelDefinition The "local" model. * @param field The "local" model field. */ constructor(model, field) { if (!isFieldAssociation(model.schema, field)) { throw new Error(`${model.schema.name}.${field} is not a relationship.`); } this.localModel = model; this._field = field; } /** * Returns a ModelRelationship for the the given model and field if the pair * indicates a relationship to another model. Else, returns `null`. * * @param model The model the relationship field exists in. * @param field The field that may relates the local model to the remote model. */ static from(model, field) { if (isFieldAssociation(model.schema, field)) { return new this(model, field); } else { return null; } } /** * Enumerates all valid `ModelRelationship`'s on the given model. * * @param model The model definition to enumerate relationships of. */ static allFrom(model) { const relationships = []; for (const field of Object.keys(model.schema.fields)) { const relationship = ModelRelationship.from(model, field); relationship && relationships.push(relationship); } return relationships; } get localDefinition() { return this.localModel.schema; } /** * The virtual/computed field on the local model that should contain * the related model. */ get field() { return this._field; } /** * The constructor that can be used to query DataStore or create instance for * the local model. */ get localConstructor() { return this.localModel.builder; } /** * The name/type of the relationship the local model has with the remote model * via the defined local model field. */ get type() { return this.localAssocation.connectionType; } /** * Raw details about the local FK as-is from the local model's field definition in * the schema. This field requires interpretation. * * @see localJoinFields * @see localAssociatedWith */ get localAssocation() { return this.localDefinition.fields[this.field].association; } /** * The field names on the local model that can be used to query or queried to match * with instances of the remote model. * * Fields are returned in-order to match the order of `this.remoteKeyFields`. */ get localJoinFields() { /** * This is relatively straightforward, actually. * * If we have explicitly stated targetNames, codegen is telling us authoritatively * to use those fields for this relationship. The local model "points to" fields * in the remote one. * * In other cases, the remote model points to this one's */ if (this.localAssocation.targetName) { // This case is theoretically unnecessary going forward. return [this.localAssocation.targetName]; } else if (this.localAssocation.targetNames) { return this.localAssocation.targetNames; } else { return this.localPKFields; } } /** * The field names on the local model that uniquely identify it. * * These fields may or may not be relevant to the join fields. */ get localPKFields() { return this.localModel.pkField; } get remoteDefinition() { return this.remoteModelType.modelConstructor?.schema; } get remoteModelType() { return this.localDefinition.fields[this.field].type; } /** * Constructor that can be used to query DataStore or create instances for * the remote model. */ get remoteModelConstructor() { return this.remoteModelType.modelConstructor.builder; } /** * The field names on the remote model that uniquely identify it. * * These fields may or may not be relevant to the join fields. */ get remotePKFields() { return this.remoteModelType.modelConstructor?.pkField || ['id']; } /** * The `associatedWith` fields from the local perspective. * * When present, these fields indicate which fields on the remote model to use * when looking for a remote association and/or determining the final remote * key fields. */ get localAssociatedWith() { if (this.localAssocation.connectionType === 'HAS_MANY' || this.localAssocation.connectionType === 'HAS_ONE') { // This de-arraying is theoretically unnecessary going forward. return Array.isArray(this.localAssocation.associatedWith) ? this.localAssocation.associatedWith : [this.localAssocation.associatedWith]; } else { return undefined; } } /** * The `remote` model's associated field's `assocation` metadata, if * present. * * This is used when determining if the remote model's associated field * specifies which FK fields to use. If this value is `undefined`, the * name of the remote field (`this.localAssociatedWith`) *is* the remote * key field. */ get explicitRemoteAssociation() { if (this.localAssociatedWith) { if (this.localAssociatedWith.length === 1) { return this.remoteDefinition.fields[this.localAssociatedWith[0]] ?.association; } else { return undefined; } } } /** * The field names on the remote model that can used to query or queried to match * with instances of the local model. * * Fields are returned in-order to match the order of `this.localKeyFields`. */ get remoteJoinFields() { /** * If the local relationship explicitly names "associated with" fields, we * need to see if this points direction to a reciprocating assocation. If it * does, the remote assocation indicates what fields to use. */ if (this.explicitRemoteAssociation?.targetName) { // This case is theoretically unnecessary going forward. return [this.explicitRemoteAssociation.targetName]; } else if (this.explicitRemoteAssociation?.targetNames) { // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain return this.explicitRemoteAssociation?.targetNames; } else if (this.localAssociatedWith) { return this.localAssociatedWith; } else { return this.remotePKFields; } } /** * Whether this relationship everything necessary to get, set, and query from * the perspective of the local model provided at instantiation. */ get isComplete() { return this.localJoinFields.length > 0 && this.remoteJoinFields.length > 0; } /** * Creates an FK mapper object with respect to the given related instance. * * E.g., if the local FK fields are `[parentId, parentName]` and point to * `[customId, name]` on the remote model, `createLocalFKObject(remote)` * will return: * * ``` * { * parentId: remote.customId, * parentName: remote.name * } * ``` * * @param remote The remote related instance. */ createLocalFKObject(remote) { const fk = {}; for (let i = 0; i < this.localJoinFields.length; i++) { fk[this.localJoinFields[i]] = remote[this.remoteJoinFields[i]]; } return fk; } /** * Creates an query mapper object to help fetch the remote instance(s) or * `null` if any of the necessary local fields are `null` or `undefined`. * * E.g., if the local FK fields are `[parentId, parentName]` and point to * `[customId, name]` on the remote model, `createLocalFKObject(remote)` * will return: * * ``` * { * customId: local.parentId * name: local.parentName * } * ``` * * If the local fields are not populated, returns * * @param local The local instance. */ createRemoteQueryObject(local) { const query = {}; for (let i = 0; i < this.remoteJoinFields.length; i++) { const localValue = local[this.localJoinFields[i]]; if (localValue === null || localValue === undefined) return null; query[this.remoteJoinFields[i]] = local[this.localJoinFields[i]]; } return query; } } export { ModelRelationship }; //# sourceMappingURL=relationship.mjs.map