Merge
mergeWorkingSet()
Section titled “mergeWorkingSet()”function
declare function mergeWorkingSet(changes: EntityChange[], links: LinkChange[], deletions: EntityDeletion[], mergeConfig: MergeConfig): Promise<MergeResult>;Writes one working set: every change, association and deletion in one transaction, or none of them.
The plain call, without a working set around it – what a service with nobody in front of it needs, and what WorkingSet#merge() runs underneath.
Parameters
changes |
rows to insert or update |
links |
associations gained and lost through many-to-many fields |
deletions |
rows to remove |
mergeConfig |
what the caller can do about a conflict |
Returns whether it landed, and what stood in the way if it did not
MergeConfig
Section titled “MergeConfig”type
type MergeConfig = { /** * true if the caller can put a conflict in front of a user and take a decision back. Only then does a * conflict come back carrying both values per field. */ conflictValues: boolean;};What the caller of a merge says about itself, as opposed to what a type declares once in the application’s MergeMetadataProvider.
MergeResult
Section titled “MergeResult”type
type MergeResult = { status: MergeStatus; conflicts: MergeConflict[];};What came of one merge. All or nothing: either everything landed or the conflicts say which rows stood in the way and nothing was written.
MergeStatus
Section titled “MergeStatus”type
type MergeStatus = "DONE" | "CONFLICT";How a merge ended: everything landed, or nothing did.
EntityChange
Section titled “EntityChange”type
type EntityChange = { type: string; id: string; version: string | null; /** * true if the row does not exist yet. Said rather than guessed: an unversioned type has no version to * read the answer off, and a row somebody else deleted must not be recreated by an update that found * nothing. */ new: boolean; changes: FieldChange[];};Everything one row changed by: the row, the base it was read at, and the fields that changed. What the user did not touch is not in here, which is what makes a concurrent change to another field mergeable.
FieldChange
Section titled “FieldChange”type
type FieldChange = { field: string; value: GenericScalar | null;};One field of one row set to one value. The value travels as a generic scalar, which is what lets one mutation write every type in the domain.
LinkChange
Section titled “LinkChange”type
type LinkChange = { type: string; id: string; field: string; added: string[]; removed: string[];};The associations one row gained and lost through one many-to-many field, by the ids of the rows on the other end. Written as link rows keyed by the pair, so it carries no link row’s id or version and never conflicts.
EntityDeletion
Section titled “EntityDeletion”type
type EntityDeletion = { type: string; id: string; version: string | null;};One row to remove, under the same optimistic lock a change is under.
MergeConflict
Section titled “MergeConflict”type
type MergeConflict = { type: string; id: string; /** * The version standing in the database now, and the base a second attempt has to be made against. Null * where the row is gone and for a type carrying no version field. */ storedVersion: string | null; /** * true if the row is not there at all. Nothing to merge into and nothing to choose between, so no * fields come with it. */ deleted: boolean; fields: MergeConflictField[];};One row the merge could not write, and why.
MergeConflictField
Section titled “MergeConflictField”type
type MergeConflictField = { field: string; mine: GenericScalar | null; stored: GenericScalar | null; /** * true if the field is here to be seen rather than decided about: the other write changed it, this one * did not, and the merge takes their value. It carries no "mine", there being no value of ours. */ informational: boolean;};One field of a row that could not be written as asked.
“mine” is the value the user typed and “stored” the one that is in the database – not “ours” and “theirs”, because whoever wrote first is gone and the only person still here is the one whose save just bounced.
MergeMeta
Section titled “MergeMeta”namespace
Imported as a namespace, and re-exported member by member from a second entry point.
MergeMeta.ManyToManyField
Section titled “MergeMeta.ManyToManyField”type ManyToManyField = { /** * The field, e.g. "bazes" on Bar. */ field: string; /** * The type the field is on, e.g. "Bar". */ sourceType: string; /** * The type whose rows the field lists, e.g. "Baz". */ targetType: string; /** * The link type the associations are rows of, e.g. "BarLink". */ linkType: string; /** * Whether the field can be written. False where a link row needs values of its own besides its two link * fields, which makes the link rows the thing to edit. */ writable: boolean;};A many-to-many field as an edit sees it: the type it is on, the type whose rows it lists, and whether it can be written.
Read from the declaration in config().meta.manyToMany. Setting bar.bazes means associating the Bar with other Bazes, which the merge writes as link rows and never as a change to Bar or Baz.
MergeMeta.MergeTypeMeta
Section titled “MergeMeta.MergeTypeMeta”type MergeTypeMeta = { /** * true if a conflict on this type comes back to the view with both values per field to resolve, rather * than failing the write. Absent where the type did not ask for it, which is the default. */ resolve?: boolean; /** * false if a concurrent change overlapping none of the user's own fields should still be shown to them * instead of merging silently. Absent means true. */ autoMerge?: boolean; /** * Fields whose change is neither recorded nor ever a conflict, alphabetically. Absent where the type * declared none. */ ignoredFields?: string[];};What a type declares about merging it, written server-side by QLive’s MergeMetadataProvider and read back through the functions below. Reached as config().meta.types[name].meta.merge, which is where DomainTypeMetaProps declares it.
Which types take part is not in here and cannot be – that is the “version” field, see isVersioned(). What a provider declares is only the part that is genuinely the application’s decision.
MergeMeta.VERSION
Section titled “MergeMeta.VERSION”declare const VERSION = "version";Name of the field that makes a type versioned, and which holds the id of the app_version record describing the state the row is in now.
MergeMeta.ignoredFields()
Section titled “MergeMeta.ignoredFields()”declare function ignoredFields(typeName: string): string[];Fields of the type whose change is neither recorded nor ever a conflict, alphabetically. Empty where the type declared none.
Parameters
typeName |
GraphQL type name, known or not |
MergeMeta.isAutoMerge()
Section titled “MergeMeta.isAutoMerge()”declare function isAutoMerge(typeName: string): boolean;Whether a concurrent change that touched none of the fields the user touched is merged without asking. True unless the type said otherwise: that case is what the whole mechanism exists to swallow.
Parameters
typeName |
GraphQL type name, known or not |
MergeMeta.isVersioned()
Section titled “MergeMeta.isVersioned()”declare function isVersioned(typeName: string): boolean;Whether rows of the given type take part in conflict detection, i.e. whether the type has a “version” field.
Nothing declares this and nothing can: the field is the declaration, and the server derives it from the same schema, so the two ends cannot disagree about who takes part. A type without one is written last-write-wins, which is what not having the column means.
Parameters
typeName |
GraphQL type name, known or not |
MergeMeta.manyToManyField()
Section titled “MergeMeta.manyToManyField()”declare function manyToManyField(typeName: string, field: string): ManyToManyField | null;The many-to-many field of the given type with the given name, or null where that field is none.
Parameters
typeName |
GraphQL type name, known or not |
field |
field name on that type |
MergeMeta.manyToManyFields()
Section titled “MergeMeta.manyToManyFields()”declare function manyToManyFields(typeName: string): ManyToManyField[];The many-to-many fields of the given type, in the order they were declared.
Parameters
typeName |
GraphQL type name, known or not |
MergeMeta.metaOf()
Section titled “MergeMeta.metaOf()”declare function metaOf(typeName: string): MergeTypeMeta;Everything the given type declared about merging it. Empty where the type declared nothing, where it is no type of the schema, or where the application registered no provider at all – which are the same answer to every reader below.
Parameters
typeName |
GraphQL type name, known or not |
MergeMeta.resolvesConflicts()
Section titled “MergeMeta.resolvesConflicts()”declare function resolvesConflicts(typeName: string): boolean;Whether the type asked for conflicts to come back to the view with both values per field, rather than failing the write. Declared, because handing a user two values and asking them to choose is a decision about the application’s UI.
Parameters
typeName |
GraphQL type name, known or not |
MergeMeta.versionedTypes()
Section titled “MergeMeta.versionedTypes()”declare function versionedTypes(): string[];Names of every versioned type in the schema, alphabetically.
MergeTypeMeta
Section titled “MergeTypeMeta”type
type MergeTypeMeta = { /** * true if a conflict on this type comes back to the view with both values per field to resolve, rather * than failing the write. Absent where the type did not ask for it, which is the default. */ resolve?: boolean; /** * false if a concurrent change overlapping none of the user's own fields should still be shown to them * instead of merging silently. Absent means true. */ autoMerge?: boolean; /** * Fields whose change is neither recorded nor ever a conflict, alphabetically. Absent where the type * declared none. */ ignoredFields?: string[];};What a type declares about merging it, written server-side by QLive’s MergeMetadataProvider and read back through the functions below. Reached as config().meta.types[name].meta.merge, which is where DomainTypeMetaProps declares it.
Which types take part is not in here and cannot be – that is the “version” field, see isVersioned(). What a provider declares is only the part that is genuinely the application’s decision.
ManyToManyField
Section titled “ManyToManyField”type
type ManyToManyField = { /** * The field, e.g. "bazes" on Bar. */ field: string; /** * The type the field is on, e.g. "Bar". */ sourceType: string; /** * The type whose rows the field lists, e.g. "Baz". */ targetType: string; /** * The link type the associations are rows of, e.g. "BarLink". */ linkType: string; /** * Whether the field can be written. False where a link row needs values of its own besides its two link * fields, which makes the link rows the thing to edit. */ writable: boolean;};A many-to-many field as an edit sees it: the type it is on, the type whose rows it lists, and whether it can be written.
Read from the declaration in config().meta.manyToMany. Setting bar.bazes means associating the Bar with other Bazes, which the merge writes as link rows and never as a change to Bar or Baz.