feat(index): support composite unique indexes in create-many upsert conflict resolution (#22604)
## Context
The `createMany` upsert path resolved conflicts by scanning individual
field
metadata and only treating a field as a conflict target when it was
flagged
`isUnique` (plus the primary `id`). This ignored **composite unique
indexes**
(multi-column unique constraints), so upserting against a multi-field
unique
key never matched an existing row and could either insert a duplicate or
fail.
## What changed
- **Conflict groups are now derived from unique indexes**, not from
per-field
`isUnique` flags. `getConflictingFields` reads the object's index
metadata
(`flatIndexMaps`) and builds one `ConflictingFieldGroup` per unique
index
(the primary `id` remains its own group).
- Each index group correctly expands its fields into DB columns,
handling:
- **Composite field types** — expands to the sub-columns included in the
unique constraint (or a specific sub-field when the index targets one).
- **`MANY_TO_ONE` relation fields** — resolves to the join column name.
- **Scalar fields** — used directly.
- `ConflictingFieldGroup.baseField: string` → **`baseFields:
string[]`**, since
a composite index spans multiple fields.
- **Clearer multi-match error message**: conflicting values are now
grouped per
index (`baseFields (fullPath: value, ...)`, groups joined by `;`) so
it's
obvious which unique key caused the ambiguity when a payload matches
different rows across different indexes.
- `CommonCreateManyQueryRunnerService` now fetches `flatIndexMaps` via
`WorkspaceManyOrAllFlatEntityMapsCacheService` and passes them into
`getConflictingFields`; the cache module is wired into
`CoreCommonApiModule`.
## Tests
- New integration suite
`composite-unique-index-upsert.integration-spec.ts`:
- single composite unique index — insert, update-on-match, and
insert-when-key-differs
- **two independent composite unique indexes** — happy path (single row
matches
both) and failure path (payload matches different rows across the two
indexes → `Multiple records found with the same unique field values` /
`BAD_USER_INPUT`).
- Updated unit specs for `get-conflicting-fields`,
`get-matching-record-id`,
`build-where-conditions`, and `categorize-records` to reflect the
index-driven grouping and the `baseFields[]` shape.
## Test plan
- [ ] `npx nx run twenty-server:test:integration:with-db-reset --
composite-unique-index-upsert`
- [ ] `npx nx test twenty-server -- get-conflicting-fields
get-matching-record-id build-where-conditions categorize-records`
- [ ] Manual: upsert against a composite unique index updates the
matching row instead of inserting a duplicate.
fixes
https://github.com/twentyhq/twenty/issues/22580#issuecomment-4894266699
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22604?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
+17
@@ -36,6 +36,7 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource';
|
||||
@@ -74,14 +75,24 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
workspaceDataSource,
|
||||
} = queryRunnerContext;
|
||||
|
||||
if (!isDefined(flatIndexMaps)) {
|
||||
throw new CommonQueryRunnerException(
|
||||
`Missing flatIndexMaps in queryRunnerContext`,
|
||||
CommonQueryRunnerExceptionCode.MISSING_FLAT_INDEX_MAPS,
|
||||
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
|
||||
);
|
||||
}
|
||||
|
||||
const objectRecords = await this.insertOrUpsertRecords({
|
||||
repository,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
args,
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
@@ -193,6 +204,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
args,
|
||||
workspaceId,
|
||||
}: {
|
||||
@@ -200,6 +212,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
|
||||
args: CommonExtendedInput<CreateManyQueryArgs>;
|
||||
workspaceId: string;
|
||||
}): Promise<InsertResult> {
|
||||
@@ -222,6 +235,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
args,
|
||||
selectedFieldsResult,
|
||||
workspaceId,
|
||||
@@ -233,6 +247,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
args,
|
||||
selectedFieldsResult,
|
||||
workspaceId,
|
||||
@@ -241,6 +256,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
|
||||
args: CreateManyQueryArgs;
|
||||
selectedFieldsResult: CommonSelectedFieldsResult;
|
||||
workspaceId: string;
|
||||
@@ -248,6 +264,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
const conflictingFieldGroups = getConflictingFields(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
const existingRecords = await this.findExistingRecords({
|
||||
repository,
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@ export type ConflictingProperty = {
|
||||
};
|
||||
|
||||
export type ConflictingFieldGroup = {
|
||||
baseField: string;
|
||||
baseFields: string[];
|
||||
conflictingProperties: ConflictingProperty[];
|
||||
};
|
||||
|
||||
+5
-5
@@ -31,7 +31,7 @@ describe('buildWhereConditions', () => {
|
||||
it('builds a single where condition for a flat field using all defined values', () => {
|
||||
const groups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'uniqueText',
|
||||
baseFields: ['uniqueText'],
|
||||
conflictingProperties: [
|
||||
{ fullPath: 'uniqueText', column: 'uniqueText' },
|
||||
],
|
||||
@@ -54,7 +54,7 @@ describe('buildWhereConditions', () => {
|
||||
it('skips adding a condition when all values for a field are undefined', () => {
|
||||
const groups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'uniqueText',
|
||||
baseFields: ['uniqueText'],
|
||||
conflictingProperties: [
|
||||
{ fullPath: 'uniqueText', column: 'uniqueText' },
|
||||
],
|
||||
@@ -69,7 +69,7 @@ describe('buildWhereConditions', () => {
|
||||
it('builds conditions for nested paths', () => {
|
||||
const groups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'emailsField',
|
||||
baseFields: ['emailsField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'emailsField.primaryEmail',
|
||||
@@ -95,13 +95,13 @@ describe('buildWhereConditions', () => {
|
||||
it('builds multiple conditions when multiple conflicting fields are provided', () => {
|
||||
const groups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'uniqueText',
|
||||
baseFields: ['uniqueText'],
|
||||
conflictingProperties: [
|
||||
{ fullPath: 'uniqueText', column: 'uniqueText' },
|
||||
],
|
||||
},
|
||||
{
|
||||
baseField: 'emailsField',
|
||||
baseFields: ['emailsField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'emailsField.primaryEmail',
|
||||
|
||||
+3
-3
@@ -7,15 +7,15 @@ import { categorizeRecords } from 'src/engine/api/common/common-query-runners/co
|
||||
describe('categorizeRecords', () => {
|
||||
const conflictingFieldGroups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'id',
|
||||
baseFields: ['id'],
|
||||
conflictingProperties: [{ fullPath: 'id', column: 'id' }],
|
||||
},
|
||||
{
|
||||
baseField: 'uniqueText',
|
||||
baseFields: ['uniqueText'],
|
||||
conflictingProperties: [{ fullPath: 'uniqueText', column: 'uniqueText' }],
|
||||
},
|
||||
{
|
||||
baseField: 'emailsField',
|
||||
baseFields: ['emailsField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'emailsField.primaryEmail',
|
||||
|
||||
+215
-41
@@ -1,8 +1,9 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
|
||||
import { getConflictingFields } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
describe('getConflictingFields', () => {
|
||||
@@ -33,50 +34,93 @@ describe('getConflictingFields', () => {
|
||||
...overrides,
|
||||
}) as FlatFieldMetadata;
|
||||
|
||||
type MockIndexField = {
|
||||
fieldMetadataId: string;
|
||||
subFieldName?: string | null;
|
||||
order?: number;
|
||||
};
|
||||
|
||||
const createMockIndex = ({
|
||||
id,
|
||||
name,
|
||||
isUnique,
|
||||
fields,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
isUnique: boolean;
|
||||
fields: MockIndexField[];
|
||||
}): FlatIndexMetadata =>
|
||||
({
|
||||
id,
|
||||
universalIdentifier: id,
|
||||
name,
|
||||
objectMetadataId,
|
||||
workspaceId,
|
||||
isUnique,
|
||||
indexWhereClause: null,
|
||||
isCustom: false,
|
||||
applicationId: null,
|
||||
flatIndexFieldMetadatas: fields.map((field, index) => ({
|
||||
id: `${id}-field-${index}`,
|
||||
universalIdentifier: `${id}-field-${index}`,
|
||||
indexMetadataId: id,
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
subFieldName: field.subFieldName ?? null,
|
||||
order: field.order ?? index,
|
||||
})),
|
||||
}) as unknown as FlatIndexMetadata;
|
||||
|
||||
const idField = createMockField({
|
||||
id: 'id-field-id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
isUnique: true,
|
||||
});
|
||||
|
||||
const uniqueTextField = createMockField({
|
||||
id: 'unique-text-id',
|
||||
name: 'uniqueText',
|
||||
type: FieldMetadataType.TEXT,
|
||||
isUnique: true,
|
||||
});
|
||||
|
||||
const emailsUniqueField = createMockField({
|
||||
id: 'emails-unique-id',
|
||||
const otherTextField = createMockField({
|
||||
id: 'other-text-id',
|
||||
name: 'otherText',
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
const emailsField = createMockField({
|
||||
id: 'emails-id',
|
||||
name: 'emailsField',
|
||||
type: FieldMetadataType.EMAILS,
|
||||
isUnique: true,
|
||||
});
|
||||
|
||||
const phonesUniqueField = createMockField({
|
||||
id: 'phones-unique-id',
|
||||
const phonesField = createMockField({
|
||||
id: 'phones-id',
|
||||
name: 'phonesField',
|
||||
type: FieldMetadataType.PHONES,
|
||||
isUnique: true,
|
||||
});
|
||||
|
||||
const phonesNotUniqueField = createMockField({
|
||||
id: 'phones-not-unique-id',
|
||||
name: 'phonesField',
|
||||
type: FieldMetadataType.PHONES,
|
||||
isUnique: false,
|
||||
});
|
||||
|
||||
const addressUniqueFieldNoIncludedProp = createMockField({
|
||||
id: 'address-unique-id',
|
||||
const addressField = createMockField({
|
||||
id: 'address-id',
|
||||
name: 'addressField',
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
isUnique: true,
|
||||
});
|
||||
|
||||
const companyRelationField = createMockField({
|
||||
id: 'company-relation-id',
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: { relationType: RelationType.MANY_TO_ONE },
|
||||
} as Partial<FlatFieldMetadata> & {
|
||||
id: string;
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
});
|
||||
|
||||
const buildFlatObjectMetadata = (
|
||||
fields: FlatFieldMetadata[],
|
||||
indexMetadataIds: string[] = [],
|
||||
): FlatObjectMetadata =>
|
||||
({
|
||||
id: objectMetadataId,
|
||||
@@ -91,8 +135,8 @@ describe('getConflictingFields', () => {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
universalIdentifier: objectMetadataId,
|
||||
fieldIds: fields.map((f) => f.id),
|
||||
indexMetadataIds: [],
|
||||
fieldIds: fields.map((field) => field.id),
|
||||
indexMetadataIds,
|
||||
viewIds: [],
|
||||
applicationId: null,
|
||||
}) as unknown as FlatObjectMetadata;
|
||||
@@ -119,24 +163,54 @@ describe('getConflictingFields', () => {
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
it('returns id and unique non-composite fields as conflicts', () => {
|
||||
const buildFlatIndexMaps = (
|
||||
indexes: FlatIndexMetadata[],
|
||||
): FlatEntityMaps<FlatIndexMetadata> => ({
|
||||
byUniversalIdentifier: indexes.reduce(
|
||||
(acc, index) => {
|
||||
acc[index.universalIdentifier] = index;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, FlatIndexMetadata>,
|
||||
),
|
||||
universalIdentifierById: indexes.reduce(
|
||||
(acc, index) => {
|
||||
acc[index.id] = index.universalIdentifier;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
),
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
it('returns id and single-field unique index conflicts', () => {
|
||||
const fields = [idField, uniqueTextField];
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields);
|
||||
const index = createMockIndex({
|
||||
id: 'unique-text-index',
|
||||
name: 'uniqueTextUniqueIndex',
|
||||
isUnique: true,
|
||||
fields: [{ fieldMetadataId: uniqueTextField.id }],
|
||||
});
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields, [index.id]);
|
||||
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
|
||||
const flatIndexMaps = buildFlatIndexMaps([index]);
|
||||
|
||||
const result = getConflictingFields(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
baseField: 'id',
|
||||
baseFields: ['id'],
|
||||
conflictingProperties: [{ fullPath: 'id', column: 'id' }],
|
||||
},
|
||||
{
|
||||
baseField: 'uniqueText',
|
||||
baseFields: ['uniqueText'],
|
||||
conflictingProperties: [
|
||||
{ fullPath: 'uniqueText', column: 'uniqueText' },
|
||||
],
|
||||
@@ -146,23 +220,31 @@ describe('getConflictingFields', () => {
|
||||
});
|
||||
|
||||
it('returns composite field with included unique property using full path and computed column', () => {
|
||||
const fields = [idField, emailsUniqueField];
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields);
|
||||
const fields = [idField, emailsField];
|
||||
const index = createMockIndex({
|
||||
id: 'emails-index',
|
||||
name: 'emailsUniqueIndex',
|
||||
isUnique: true,
|
||||
fields: [{ fieldMetadataId: emailsField.id }],
|
||||
});
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields, [index.id]);
|
||||
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
|
||||
const flatIndexMaps = buildFlatIndexMaps([index]);
|
||||
|
||||
const result = getConflictingFields(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
baseField: 'id',
|
||||
baseFields: ['id'],
|
||||
conflictingProperties: [{ fullPath: 'id', column: 'id' }],
|
||||
},
|
||||
{
|
||||
baseField: 'emailsField',
|
||||
baseFields: ['emailsField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'emailsField.primaryEmail',
|
||||
@@ -175,22 +257,30 @@ describe('getConflictingFields', () => {
|
||||
});
|
||||
|
||||
it('returns every included unique property for phone composite fields', () => {
|
||||
const fields = [idField, phonesUniqueField];
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields);
|
||||
const fields = [idField, phonesField];
|
||||
const index = createMockIndex({
|
||||
id: 'phones-index',
|
||||
name: 'phonesUniqueIndex',
|
||||
isUnique: true,
|
||||
fields: [{ fieldMetadataId: phonesField.id }],
|
||||
});
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields, [index.id]);
|
||||
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
|
||||
const flatIndexMaps = buildFlatIndexMaps([index]);
|
||||
|
||||
const result = getConflictingFields(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
baseField: 'id',
|
||||
baseFields: ['id'],
|
||||
conflictingProperties: [{ fullPath: 'id', column: 'id' }],
|
||||
},
|
||||
{
|
||||
baseField: 'phonesField',
|
||||
baseFields: ['phonesField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'phonesField.primaryPhoneNumber',
|
||||
@@ -209,39 +299,123 @@ describe('getConflictingFields', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not include composite fields without included unique property', () => {
|
||||
const fields = [idField, addressUniqueFieldNoIncludedProp];
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields);
|
||||
it('skips composite unique index without included unique property', () => {
|
||||
const fields = [idField, addressField];
|
||||
const index = createMockIndex({
|
||||
id: 'address-index',
|
||||
name: 'addressUniqueIndex',
|
||||
isUnique: true,
|
||||
fields: [{ fieldMetadataId: addressField.id }],
|
||||
});
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields, [index.id]);
|
||||
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
|
||||
const flatIndexMaps = buildFlatIndexMaps([index]);
|
||||
|
||||
const result = getConflictingFields(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
baseField: 'id',
|
||||
baseFields: ['id'],
|
||||
conflictingProperties: [{ fullPath: 'id', column: 'id' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores non-unique fields', () => {
|
||||
const fields = [idField, phonesNotUniqueField];
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields);
|
||||
it('ignores non-unique indexes', () => {
|
||||
const fields = [idField, uniqueTextField];
|
||||
const index = createMockIndex({
|
||||
id: 'non-unique-index',
|
||||
name: 'nonUniqueIndex',
|
||||
isUnique: false,
|
||||
fields: [{ fieldMetadataId: uniqueTextField.id }],
|
||||
});
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields, [index.id]);
|
||||
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
|
||||
const flatIndexMaps = buildFlatIndexMaps([index]);
|
||||
|
||||
const result = getConflictingFields(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
baseField: 'id',
|
||||
baseFields: ['id'],
|
||||
conflictingProperties: [{ fullPath: 'id', column: 'id' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns a single group with every column of a composite (multi-field) unique index, ordered', () => {
|
||||
const fields = [idField, uniqueTextField, otherTextField];
|
||||
const index = createMockIndex({
|
||||
id: 'composite-index',
|
||||
name: 'uniqueTextOtherTextUniqueIndex',
|
||||
isUnique: true,
|
||||
fields: [
|
||||
{ fieldMetadataId: otherTextField.id, order: 1 },
|
||||
{ fieldMetadataId: uniqueTextField.id, order: 0 },
|
||||
],
|
||||
});
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields, [index.id]);
|
||||
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
|
||||
const flatIndexMaps = buildFlatIndexMaps([index]);
|
||||
|
||||
const result = getConflictingFields(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
baseFields: ['uniqueText', 'otherText'],
|
||||
conflictingProperties: [
|
||||
{ fullPath: 'uniqueText', column: 'uniqueText' },
|
||||
{ fullPath: 'otherText', column: 'otherText' },
|
||||
],
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves relation fields to their join column in a composite unique index', () => {
|
||||
const fields = [idField, companyRelationField, uniqueTextField];
|
||||
const index = createMockIndex({
|
||||
id: 'company-text-index',
|
||||
name: 'companyIdUniqueTextUniqueIndex',
|
||||
isUnique: true,
|
||||
fields: [
|
||||
{ fieldMetadataId: companyRelationField.id, order: 0 },
|
||||
{ fieldMetadataId: uniqueTextField.id, order: 1 },
|
||||
],
|
||||
});
|
||||
const flatObjectMetadata = buildFlatObjectMetadata(fields, [index.id]);
|
||||
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields);
|
||||
const flatIndexMaps = buildFlatIndexMaps([index]);
|
||||
|
||||
const result = getConflictingFields(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
baseFields: ['company', 'uniqueText'],
|
||||
conflictingProperties: [
|
||||
{ fullPath: 'companyId', column: 'companyId' },
|
||||
{ fullPath: 'uniqueText', column: 'uniqueText' },
|
||||
],
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+8
-8
@@ -32,7 +32,7 @@ describe('getMatchingRecordId', () => {
|
||||
|
||||
const conflictingFieldGroups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'emailsField',
|
||||
baseFields: ['emailsField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'emailsField.primaryEmail',
|
||||
@@ -61,7 +61,7 @@ describe('getMatchingRecordId', () => {
|
||||
|
||||
const conflictingFieldGroups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'phonesField',
|
||||
baseFields: ['phonesField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'phonesField.primaryPhoneNumber',
|
||||
@@ -94,7 +94,7 @@ describe('getMatchingRecordId', () => {
|
||||
|
||||
const conflictingFieldGroups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'phonesField',
|
||||
baseFields: ['phonesField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'phonesField.primaryPhoneNumber',
|
||||
@@ -124,7 +124,7 @@ describe('getMatchingRecordId', () => {
|
||||
|
||||
const conflictingFieldGroups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'emailsField',
|
||||
baseFields: ['emailsField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'emailsField.primaryEmail',
|
||||
@@ -151,11 +151,11 @@ describe('getMatchingRecordId', () => {
|
||||
|
||||
const conflictingFieldGroups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'id',
|
||||
baseFields: ['id'],
|
||||
conflictingProperties: [{ fullPath: 'id', column: 'id' }],
|
||||
},
|
||||
{
|
||||
baseField: 'uniqueText',
|
||||
baseFields: ['uniqueText'],
|
||||
conflictingProperties: [
|
||||
{ fullPath: 'uniqueText', column: 'uniqueText' },
|
||||
],
|
||||
@@ -179,13 +179,13 @@ describe('getMatchingRecordId', () => {
|
||||
|
||||
const conflictingFieldGroups: ConflictingFieldGroup[] = [
|
||||
{
|
||||
baseField: 'uniqueText',
|
||||
baseFields: ['uniqueText'],
|
||||
conflictingProperties: [
|
||||
{ fullPath: 'uniqueText', column: 'uniqueText' },
|
||||
],
|
||||
},
|
||||
{
|
||||
baseField: 'emailsField',
|
||||
baseFields: ['emailsField'],
|
||||
conflictingProperties: [
|
||||
{
|
||||
fullPath: 'emailsField.primaryEmail',
|
||||
|
||||
+156
-23
@@ -1,39 +1,172 @@
|
||||
import { compositeTypeDefinitions } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { compositeTypeDefinitions, RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ConflictingFieldGroup } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/types/conflicting-field-group.type';
|
||||
import {
|
||||
type ConflictingFieldGroup,
|
||||
type ConflictingProperty,
|
||||
} from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/types/conflicting-field-group.type';
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import {
|
||||
type FlatIndexFieldMetadata,
|
||||
type FlatIndexMetadata,
|
||||
} from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
const computeConflictingPropertiesForIndexField = ({
|
||||
flatFieldMetadata,
|
||||
subFieldName,
|
||||
}: {
|
||||
flatFieldMetadata: FlatFieldMetadata;
|
||||
subFieldName: string | null;
|
||||
}): ConflictingProperty[] | undefined => {
|
||||
if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
|
||||
if (flatFieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const joinColumn = computeMorphOrRelationFieldJoinColumnName({
|
||||
name: flatFieldMetadata.name,
|
||||
});
|
||||
|
||||
return [{ fullPath: joinColumn, column: joinColumn }];
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(flatFieldMetadata.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(flatFieldMetadata.type);
|
||||
|
||||
if (!isDefined(compositeType)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isDefined(subFieldName)) {
|
||||
const property = compositeType.properties.find(
|
||||
(compositeProperty) => compositeProperty.name === subFieldName,
|
||||
);
|
||||
|
||||
if (!isDefined(property)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
fullPath: `${flatFieldMetadata.name}.${property.name}`,
|
||||
column: computeCompositeColumnName(
|
||||
{ name: flatFieldMetadata.name, type: flatFieldMetadata.type },
|
||||
property,
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return compositeType.properties
|
||||
.filter((property) => property.isIncludedInUniqueConstraint)
|
||||
.map((property) => ({
|
||||
fullPath: `${flatFieldMetadata.name}.${property.name}`,
|
||||
column: computeCompositeColumnName(
|
||||
{ name: flatFieldMetadata.name, type: flatFieldMetadata.type },
|
||||
property,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
return [{ fullPath: flatFieldMetadata.name, column: flatFieldMetadata.name }];
|
||||
};
|
||||
|
||||
const computeConflictingPropertiesForIndex = ({
|
||||
flatIndexFieldMetadatas,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
flatIndexFieldMetadatas: FlatIndexFieldMetadata[];
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}):
|
||||
| { baseFields: string[]; conflictingProperties: ConflictingProperty[] }
|
||||
| undefined => {
|
||||
const orderedIndexFields = [...flatIndexFieldMetadatas].sort(
|
||||
(a, b) => a.order - b.order,
|
||||
);
|
||||
|
||||
const baseFields: string[] = [];
|
||||
const conflictingProperties: ConflictingProperty[] = [];
|
||||
|
||||
for (const indexField of orderedIndexFields) {
|
||||
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: indexField.fieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const propertiesForField = computeConflictingPropertiesForIndexField({
|
||||
flatFieldMetadata,
|
||||
subFieldName: indexField.subFieldName,
|
||||
});
|
||||
|
||||
if (!isDefined(propertiesForField) || propertiesForField.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!baseFields.includes(flatFieldMetadata.name)) {
|
||||
baseFields.push(flatFieldMetadata.name);
|
||||
}
|
||||
|
||||
conflictingProperties.push(...propertiesForField);
|
||||
}
|
||||
|
||||
return { baseFields, conflictingProperties };
|
||||
};
|
||||
|
||||
export const getConflictingFields = (
|
||||
flatObjectMetadata: FlatObjectMetadata,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>,
|
||||
): ConflictingFieldGroup[] => {
|
||||
return getFlatFieldsFromFlatObjectMetadata(
|
||||
const conflictingFieldGroups: ConflictingFieldGroup[] = [];
|
||||
|
||||
const idField = getFlatFieldsFromFlatObjectMetadata(
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
)
|
||||
.filter((field) => field.isUnique || field.name === 'id')
|
||||
.map((field) => {
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
).find((field) => field.name === 'id');
|
||||
|
||||
if (!compositeType) {
|
||||
return {
|
||||
baseField: field.name,
|
||||
conflictingProperties: [{ fullPath: field.name, column: field.name }],
|
||||
};
|
||||
}
|
||||
if (isDefined(idField)) {
|
||||
conflictingFieldGroups.push({
|
||||
baseFields: ['id'],
|
||||
conflictingProperties: [{ fullPath: 'id', column: 'id' }],
|
||||
});
|
||||
}
|
||||
|
||||
const conflictingProperties = compositeType.properties
|
||||
.filter((prop) => prop.isIncludedInUniqueConstraint)
|
||||
.map((property) => ({
|
||||
fullPath: `${field.name}.${property.name}`,
|
||||
column: `${field.name}${capitalize(property.name)}`,
|
||||
}));
|
||||
const uniqueIndexes = findManyFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityIds: flatObjectMetadata.indexMetadataIds,
|
||||
flatEntityMaps: flatIndexMaps,
|
||||
}).filter((flatIndexMetadata) => flatIndexMetadata.isUnique);
|
||||
|
||||
return { baseField: field.name, conflictingProperties };
|
||||
})
|
||||
.filter((group) => group.conflictingProperties.length > 0);
|
||||
for (const flatIndexMetadata of uniqueIndexes) {
|
||||
const indexConflictingFields = computeConflictingPropertiesForIndex({
|
||||
flatIndexFieldMetadatas: flatIndexMetadata.flatIndexFieldMetadatas,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(indexConflictingFields) ||
|
||||
indexConflictingFields.conflictingProperties.length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
conflictingFieldGroups.push({
|
||||
baseFields: indexConflictingFields.baseFields,
|
||||
conflictingProperties: indexConflictingFields.conflictingProperties,
|
||||
});
|
||||
}
|
||||
|
||||
return conflictingFieldGroups;
|
||||
};
|
||||
|
||||
+19
-7
@@ -50,16 +50,28 @@ export const getMatchingRecordId = (
|
||||
|
||||
if ([...new Set(matchingRecordIds)].length > 1) {
|
||||
const conflictingFieldsValues = conflictingFieldGroups
|
||||
.flatMap((group) => group.conflictingProperties)
|
||||
.map((conflictingProperty) => {
|
||||
const value = getValueFromPath(record, conflictingProperty.fullPath);
|
||||
.map((group) => {
|
||||
const values = group.conflictingProperties
|
||||
.map((conflictingProperty) => {
|
||||
const value = getValueFromPath(
|
||||
record,
|
||||
conflictingProperty.fullPath,
|
||||
);
|
||||
|
||||
return isDefined(value)
|
||||
? `${conflictingProperty.fullPath}: ${value}`
|
||||
: undefined;
|
||||
return isDefined(value)
|
||||
? `${conflictingProperty.fullPath}: ${value}`
|
||||
: undefined;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (values.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${group.baseFields.join(', ')} (${values.join(', ')})`;
|
||||
})
|
||||
.filter(isDefined)
|
||||
.join(', ');
|
||||
.join('; ');
|
||||
|
||||
throw new CommonQueryRunnerException(
|
||||
`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import { type MessageDescriptor } from '@lingui/core';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum CommonQueryRunnerExceptionCode {
|
||||
MISSING_FLAT_INDEX_MAPS = 'MISSING_FLAT_INDEX_MAPS',
|
||||
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
|
||||
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
|
||||
INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT',
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = (
|
||||
throw new UserInputError(error);
|
||||
case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT:
|
||||
throw new AuthenticationError(error);
|
||||
case CommonQueryRunnerExceptionCode.MISSING_FLAT_INDEX_MAPS:
|
||||
case CommonQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD:
|
||||
case CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
throw new InternalServerError(error);
|
||||
|
||||
+1
@@ -34,6 +34,7 @@ export const commonQueryRunnerToRestApiExceptionHandler = (
|
||||
throw new NotFoundException('Record not found');
|
||||
case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT:
|
||||
throw new UnauthorizedException(error.message);
|
||||
case CommonQueryRunnerExceptionCode.MISSING_FLAT_INDEX_MAPS:
|
||||
case CommonQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD:
|
||||
case CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
throw new InternalServerErrorException(error.message);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
@@ -44,6 +45,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
RecordTransformerModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [
|
||||
ProcessNestedRelationsHelper,
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
export type CommonBaseQueryRunnerContext = {
|
||||
@@ -8,5 +9,6 @@ export type CommonBaseQueryRunnerContext = {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatIndexMaps?: FlatEntityMaps<FlatIndexMetadata>;
|
||||
objectIdByNameSingular: Record<string, string>;
|
||||
};
|
||||
|
||||
+3
@@ -206,10 +206,12 @@ export class DirectExecutionService {
|
||||
graphQLResolverNameMap,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'graphQLResolverNameMap',
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
]);
|
||||
|
||||
const { idByNameSingular: objectIdByNameSingular } =
|
||||
@@ -235,6 +237,7 @@ export class DirectExecutionService {
|
||||
entry,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
);
|
||||
|
||||
|
||||
+3
@@ -2,12 +2,14 @@ import { type ResolverNameMapEntry } from 'src/engine/api/graphql/direct-executi
|
||||
import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
export const buildWorkspaceSchemaBuilderContext = (
|
||||
entry: ResolverNameMapEntry,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>,
|
||||
objectIdByNameSingular: Record<string, string>,
|
||||
): WorkspaceSchemaBuilderContext => {
|
||||
const flatObjectMetadata =
|
||||
@@ -25,6 +27,7 @@ export const buildWorkspaceSchemaBuilderContext = (
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
};
|
||||
};
|
||||
|
||||
+2
@@ -25,6 +25,7 @@ export type WorkspaceGraphqlSchemaSDLResult = {
|
||||
usedScalarNames: string[];
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -168,6 +169,7 @@ export class WorkspaceGraphqlSchemaSDLService {
|
||||
usedScalarNames,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -14,6 +14,7 @@ import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-reso
|
||||
import { WorkspaceResolverBuilderService } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||
|
||||
@@ -56,6 +57,7 @@ export class WorkspaceResolverFactory {
|
||||
async create(
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>,
|
||||
objectIdByNameSingular: Record<string, string>,
|
||||
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
|
||||
): Promise<IResolvers> {
|
||||
@@ -113,6 +115,7 @@ export class WorkspaceResolverFactory {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
});
|
||||
}
|
||||
@@ -144,6 +147,7 @@ export class WorkspaceResolverFactory {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
});
|
||||
}
|
||||
|
||||
+2
@@ -1,10 +1,12 @@
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
export interface WorkspaceSchemaBuilderContext {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
|
||||
objectIdByNameSingular: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export class WorkspaceSchemaFactory {
|
||||
usedScalarNames,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
} = schemaSDLResult;
|
||||
|
||||
const { idByNameSingular } = buildObjectIdByNameMaps(
|
||||
@@ -48,6 +49,7 @@ export class WorkspaceSchemaFactory {
|
||||
const autoGeneratedResolvers = await this.workspaceResolverFactory.create(
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
idByNameSingular,
|
||||
workspaceResolverBuilderMethodNames,
|
||||
);
|
||||
|
||||
@@ -159,6 +159,7 @@ export abstract class RestApiBaseHandler {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
} = await this.getObjectMetadata(request, parsedObject);
|
||||
|
||||
@@ -169,6 +170,7 @@ export abstract class RestApiBaseHandler {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
};
|
||||
}
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
} = await this.buildCommonOptions(request);
|
||||
|
||||
@@ -45,6 +46,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
},
|
||||
);
|
||||
|
||||
+2
@@ -27,6 +27,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
} = await this.buildCommonOptions(request);
|
||||
|
||||
@@ -46,6 +47,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
objectIdByNameSingular,
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user