Add default relation to standard object on custom object in manifest (#18033)
add default relation fields when creating an object from an application --------- Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
@@ -11,7 +11,11 @@ import {
|
||||
import { createTypecheckPlugin } from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
import * as esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import { OUTPUT_DIR, NODE_ESM_CJS_BANNER } from 'twenty-shared/application';
|
||||
import {
|
||||
OUTPUT_DIR,
|
||||
NODE_ESM_CJS_BANNER,
|
||||
GENERATED_DIR,
|
||||
} from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
export const LOGIC_FUNCTION_EXTERNAL_MODULES: string[] = [
|
||||
@@ -199,7 +203,7 @@ const createSdkGeneratedResolverPlugin = (appPath: string): esbuild.Plugin => ({
|
||||
appPath,
|
||||
'node_modules',
|
||||
'twenty-sdk',
|
||||
'generated',
|
||||
GENERATED_DIR,
|
||||
'index.ts',
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING,
|
||||
} from '@/cli/utilities/build/manifest/manifest-extract-config';
|
||||
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
|
||||
import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields';
|
||||
import {
|
||||
type ApplicationConfig,
|
||||
type FrontComponentConfig,
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
} from 'twenty-shared/application';
|
||||
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
|
||||
|
||||
const loadSources = async (appPath: string): Promise<string[]> => {
|
||||
return await glob(['**/*.ts', '**/*.tsx'], {
|
||||
@@ -120,13 +120,14 @@ export const buildManifest = async (
|
||||
filePath,
|
||||
});
|
||||
|
||||
const objectFieldsWithDefaultFields = injectDefaultFieldsInObjectFields(
|
||||
extract.config,
|
||||
);
|
||||
const {
|
||||
objectFields: objectFieldsWithDefaults,
|
||||
fields: reverseRelationFields,
|
||||
} = getDefaultFieldsInObjectFields(extract.config);
|
||||
|
||||
const labelIdentifierFieldMetadataUniversalIdentifier =
|
||||
extract.config.labelIdentifierFieldMetadataUniversalIdentifier ??
|
||||
objectFieldsWithDefaultFields.find((field) => field.name === 'name')
|
||||
objectFieldsWithDefaults.find((field) => field.name === 'name')
|
||||
?.universalIdentifier;
|
||||
|
||||
if (!labelIdentifierFieldMetadataUniversalIdentifier) {
|
||||
@@ -138,11 +139,12 @@ export const buildManifest = async (
|
||||
|
||||
const objectManifest: ObjectManifest = {
|
||||
...extract.config,
|
||||
fields: objectFieldsWithDefaultFields,
|
||||
fields: objectFieldsWithDefaults,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
};
|
||||
|
||||
objects.push(objectManifest);
|
||||
fields.push(...reverseRelationFields);
|
||||
|
||||
errors.push(...extract.errors);
|
||||
objectsFilePaths.push(relativePath);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import crypto from 'crypto';
|
||||
import { relative } from 'path';
|
||||
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
|
||||
import {
|
||||
type Manifest,
|
||||
OUTPUT_DIR,
|
||||
API_CLIENT_DIR,
|
||||
} from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import type { EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config';
|
||||
@@ -97,7 +101,7 @@ export const manifestUpdateChecksums = ({
|
||||
|
||||
if (
|
||||
fileFolder === FileFolder.Dependencies &&
|
||||
rootBuiltPath.startsWith('api-client/')
|
||||
rootBuiltPath.startsWith(`${API_CLIENT_DIR}/`)
|
||||
) {
|
||||
const entry = builtFileInfos.get(builtPath);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path, { relative } from 'path';
|
||||
import chokidar, { type FSWatcher } from 'chokidar';
|
||||
import { type EventName } from 'chokidar/handler.js';
|
||||
import { ASSETS_DIR } from 'twenty-shared/application';
|
||||
import { ASSETS_DIR, GENERATED_DIR } from 'twenty-shared/application';
|
||||
|
||||
export type ManifestWatcherOptions = {
|
||||
appPath: string;
|
||||
@@ -10,7 +10,7 @@ export type ManifestWatcherOptions = {
|
||||
|
||||
const IGNORED_DIRECTORY_NAMES = new Set([
|
||||
'node_modules',
|
||||
'generated',
|
||||
GENERATED_DIR,
|
||||
'dist',
|
||||
'.twenty',
|
||||
]);
|
||||
|
||||
+53
-23
@@ -1,7 +1,9 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields';
|
||||
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields';
|
||||
import { type ObjectFieldManifest } from 'twenty-shared/application';
|
||||
|
||||
const baseObjectConfig: ObjectConfig = {
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
@@ -12,16 +14,25 @@ const baseObjectConfig: ObjectConfig = {
|
||||
fields: [],
|
||||
};
|
||||
|
||||
describe('injectDefaultFieldsInObjectFields', () => {
|
||||
describe('getDefaultFieldsInObjectFields', () => {
|
||||
it('should return all default fields when objectConfig has no fields', () => {
|
||||
const result = injectDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
const { objectFields, fields } =
|
||||
getDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
const defaultFields = getDefaultObjectFields(baseObjectConfig);
|
||||
const {
|
||||
objectFields: defaultRelationObjectFields,
|
||||
fields: expectedReverseFields,
|
||||
} = getDefaultRelationObjectFields(baseObjectConfig);
|
||||
|
||||
expect(result).toEqual(defaultFields);
|
||||
expect(objectFields).toEqual([
|
||||
...defaultFields,
|
||||
...defaultRelationObjectFields,
|
||||
]);
|
||||
expect(fields).toEqual(expectedReverseFields);
|
||||
});
|
||||
|
||||
it('should preserve custom fields and append missing default fields', () => {
|
||||
const customField = {
|
||||
const customField: ObjectFieldManifest = {
|
||||
universalIdentifier: '11111111-1111-1111-1111-111111111111',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
@@ -33,15 +44,19 @@ describe('injectDefaultFieldsInObjectFields', () => {
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const result = injectDefaultFieldsInObjectFields(objectConfig);
|
||||
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getDefaultObjectFields(objectConfig);
|
||||
const { objectFields: defaultRelationObjectFields } =
|
||||
getDefaultRelationObjectFields(objectConfig);
|
||||
|
||||
expect(result[0]).toEqual(customField);
|
||||
expect(result).toHaveLength(1 + defaultFields.length);
|
||||
expect(objectFields[0]).toEqual(customField);
|
||||
expect(objectFields).toHaveLength(
|
||||
1 + defaultFields.length + defaultRelationObjectFields.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not inject a default field when a field with the same name exists', () => {
|
||||
const customIdField = {
|
||||
const customIdField: ObjectFieldManifest = {
|
||||
universalIdentifier: '22222222-2222-2222-2222-222222222222',
|
||||
name: 'id',
|
||||
label: 'Custom Id',
|
||||
@@ -53,16 +68,16 @@ describe('injectDefaultFieldsInObjectFields', () => {
|
||||
fields: [customIdField],
|
||||
};
|
||||
|
||||
const result = injectDefaultFieldsInObjectFields(objectConfig);
|
||||
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
const idFields = result.filter((f) => f.name === 'id');
|
||||
const idFields = objectFields.filter((f) => f.name === 'id');
|
||||
|
||||
expect(idFields).toHaveLength(1);
|
||||
expect(idFields[0]).toEqual(customIdField);
|
||||
});
|
||||
|
||||
it('should skip multiple default fields when overridden by custom fields', () => {
|
||||
const customFields = [
|
||||
const customFields: ObjectFieldManifest[] = [
|
||||
{
|
||||
universalIdentifier: '33333333-3333-3333-3333-333333333333',
|
||||
name: 'id',
|
||||
@@ -88,23 +103,28 @@ describe('injectDefaultFieldsInObjectFields', () => {
|
||||
fields: customFields,
|
||||
};
|
||||
|
||||
const result = injectDefaultFieldsInObjectFields(objectConfig);
|
||||
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getDefaultObjectFields(objectConfig);
|
||||
const { objectFields: defaultRelationObjectFields } =
|
||||
getDefaultRelationObjectFields(objectConfig);
|
||||
const overriddenCount = defaultFields.filter((df) =>
|
||||
customFields.some((cf) => cf.name === df.name),
|
||||
).length;
|
||||
|
||||
expect(result).toHaveLength(
|
||||
customFields.length + defaultFields.length - overriddenCount,
|
||||
expect(objectFields).toHaveLength(
|
||||
customFields.length +
|
||||
defaultFields.length +
|
||||
defaultRelationObjectFields.length -
|
||||
overriddenCount,
|
||||
);
|
||||
|
||||
expect(result.filter((f) => f.name === 'id')).toHaveLength(1);
|
||||
expect(result.filter((f) => f.name === 'name')).toHaveLength(1);
|
||||
expect(result.filter((f) => f.name === 'position')).toHaveLength(1);
|
||||
expect(objectFields.filter((f) => f.name === 'id')).toHaveLength(1);
|
||||
expect(objectFields.filter((f) => f.name === 'name')).toHaveLength(1);
|
||||
expect(objectFields.filter((f) => f.name === 'position')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should place custom fields before default fields in the result', () => {
|
||||
const customField = {
|
||||
const customField: ObjectFieldManifest = {
|
||||
universalIdentifier: '66666666-6666-6666-6666-666666666666',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
@@ -116,13 +136,13 @@ describe('injectDefaultFieldsInObjectFields', () => {
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const result = injectDefaultFieldsInObjectFields(objectConfig);
|
||||
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
expect(result[0]).toEqual(customField);
|
||||
expect(objectFields[0]).toEqual(customField);
|
||||
});
|
||||
|
||||
it('should not mutate the original objectConfig fields array', () => {
|
||||
const customField = {
|
||||
const customField: ObjectFieldManifest = {
|
||||
universalIdentifier: '77777777-7777-7777-7777-777777777777',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
@@ -136,8 +156,18 @@ describe('injectDefaultFieldsInObjectFields', () => {
|
||||
|
||||
const originalLength = objectConfig.fields.length;
|
||||
|
||||
injectDefaultFieldsInObjectFields(objectConfig);
|
||||
getDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
expect(objectConfig.fields).toHaveLength(originalLength);
|
||||
});
|
||||
|
||||
it('should return reverse relation fields for each default relation', () => {
|
||||
const { fields } = getDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
|
||||
expect(fields).toHaveLength(5);
|
||||
|
||||
const fieldNames = fields.map((f) => f.name);
|
||||
|
||||
expect(fieldNames).toContain('targetTestObject');
|
||||
});
|
||||
});
|
||||
+56
-7
@@ -22,7 +22,7 @@ describe('getDefaultObjectFields', () => {
|
||||
it('should return an array of 9 default fields', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
|
||||
expect(fields).toHaveLength(7);
|
||||
expect(fields).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('should include an id field with UUID type', () => {
|
||||
@@ -59,15 +59,26 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should include updatedAt and deletedAt fields with DATE_TIME type', () => {
|
||||
it('should include createdAt, updatedAt and deletedAt fields with DATE_TIME type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const updatedAtFields = fields.filter(
|
||||
(field) => field.name === 'updatedAt',
|
||||
);
|
||||
const createdAtField = fields.find((field) => field.name === 'createdAt');
|
||||
const updatedAtField = fields.find((field) => field.name === 'updatedAt');
|
||||
const deletedAtField = fields.find((field) => field.name === 'deletedAt');
|
||||
|
||||
expect(updatedAtFields).toHaveLength(1);
|
||||
expect(updatedAtFields[0]).toEqual({
|
||||
expect(createdAtField).toBeDefined();
|
||||
expect(createdAtField).toEqual({
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: expectedUniversalId('createdAt'),
|
||||
});
|
||||
|
||||
expect(updatedAtField).toBeDefined();
|
||||
expect(updatedAtField).toEqual({
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
description: 'Last time the record was changed',
|
||||
@@ -121,6 +132,42 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a position field with POSITION type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const positionField = fields.find((field) => field.name === 'position');
|
||||
|
||||
expect(positionField).toBeDefined();
|
||||
expect(positionField).toEqual({
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
defaultValue: 0,
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: expectedUniversalId('position'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a searchVector field with TS_VECTOR type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const searchVectorField = fields.find(
|
||||
(field) => field.name === 'searchVector',
|
||||
);
|
||||
|
||||
expect(searchVectorField).toBeDefined();
|
||||
expect(searchVectorField).toEqual({
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
description: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: expectedUniversalId('searchVector'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate deterministic universalIdentifiers based on objectConfig', () => {
|
||||
const firstResult = getDefaultObjectFields(mockObjectConfig);
|
||||
const secondResult = getDefaultObjectFields(mockObjectConfig);
|
||||
@@ -160,6 +207,8 @@ describe('getDefaultObjectFields', () => {
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'position',
|
||||
'searchVector',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields';
|
||||
import {
|
||||
type FieldManifest,
|
||||
type ObjectFieldManifest,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
export const getDefaultFieldsInObjectFields = (
|
||||
objectConfig: ObjectConfig,
|
||||
): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => {
|
||||
const defaultObjectFields = getDefaultObjectFields(objectConfig);
|
||||
const { objectFields: defaultRelationObjectFields, fields: reverseFields } =
|
||||
getDefaultRelationObjectFields(objectConfig);
|
||||
|
||||
const objectConfigFieldNames = (objectConfig.fields ?? []).map((f) => f.name);
|
||||
|
||||
const objectFieldsWithDefaults = [...objectConfig.fields];
|
||||
|
||||
for (const defaultField of defaultObjectFields) {
|
||||
if (!objectConfigFieldNames.includes(defaultField.name)) {
|
||||
objectFieldsWithDefaults.push(defaultField);
|
||||
}
|
||||
}
|
||||
|
||||
for (const defaultRelationField of defaultRelationObjectFields) {
|
||||
if (!objectConfigFieldNames.includes(defaultRelationField.name)) {
|
||||
objectFieldsWithDefaults.push(defaultRelationField);
|
||||
}
|
||||
}
|
||||
|
||||
return { objectFields: objectFieldsWithDefaults, fields: reverseFields };
|
||||
};
|
||||
+39
-9
@@ -1,12 +1,12 @@
|
||||
import { type ObjectManifest } from 'twenty-shared/application';
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
|
||||
import { type ObjectFieldManifest } from 'twenty-shared/application';
|
||||
|
||||
export const getDefaultObjectFields = (
|
||||
objectConfig: ObjectConfig,
|
||||
): ObjectManifest['fields'] => {
|
||||
const idField = {
|
||||
): ObjectFieldManifest[] => {
|
||||
const idField: ObjectFieldManifest = {
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
@@ -20,7 +20,7 @@ export const getDefaultObjectFields = (
|
||||
}),
|
||||
};
|
||||
|
||||
const nameField = {
|
||||
const nameField: ObjectFieldManifest = {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name',
|
||||
@@ -34,7 +34,7 @@ export const getDefaultObjectFields = (
|
||||
}),
|
||||
};
|
||||
|
||||
const createdAtField = {
|
||||
const createdAtField: ObjectFieldManifest = {
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
@@ -48,7 +48,7 @@ export const getDefaultObjectFields = (
|
||||
}),
|
||||
};
|
||||
|
||||
const updatedAtField = {
|
||||
const updatedAtField: ObjectFieldManifest = {
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
description: 'Last time the record was changed',
|
||||
@@ -62,7 +62,7 @@ export const getDefaultObjectFields = (
|
||||
}),
|
||||
};
|
||||
|
||||
const deletedAtField = {
|
||||
const deletedAtField: ObjectFieldManifest = {
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
description: 'Deletion date',
|
||||
@@ -76,7 +76,7 @@ export const getDefaultObjectFields = (
|
||||
}),
|
||||
};
|
||||
|
||||
const createdByField = {
|
||||
const createdByField: ObjectFieldManifest = {
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
description: 'The creator of the record',
|
||||
@@ -90,7 +90,7 @@ export const getDefaultObjectFields = (
|
||||
}),
|
||||
};
|
||||
|
||||
const updatedByField = {
|
||||
const updatedByField: ObjectFieldManifest = {
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
description: 'The workspace member who last updated the record',
|
||||
@@ -104,6 +104,34 @@ export const getDefaultObjectFields = (
|
||||
}),
|
||||
};
|
||||
|
||||
const positionField: ObjectFieldManifest = {
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
defaultValue: 0,
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'position',
|
||||
}),
|
||||
};
|
||||
|
||||
const searchVectorField: ObjectFieldManifest = {
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
description: 'Search vector',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'searchVector',
|
||||
}),
|
||||
};
|
||||
|
||||
return [
|
||||
idField,
|
||||
nameField,
|
||||
@@ -112,5 +140,7 @@ export const getDefaultObjectFields = (
|
||||
deletedAtField,
|
||||
createdByField,
|
||||
updatedByField,
|
||||
positionField,
|
||||
searchVectorField,
|
||||
];
|
||||
};
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
|
||||
import { RelationType } from '@/sdk';
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import {
|
||||
type FieldManifest,
|
||||
type ObjectFieldManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
type DefaultRelationConfig = {
|
||||
fieldName: string;
|
||||
label: string;
|
||||
targetFieldName: (objectConfig: ObjectConfig) => string;
|
||||
targetLabel: (objectConfig: ObjectConfig) => string;
|
||||
icon: string;
|
||||
targetIcon: string;
|
||||
standardObjectKey: keyof typeof STANDARD_OBJECTS;
|
||||
} & (
|
||||
| { targetFieldType: FieldMetadataType.RELATION }
|
||||
| { targetFieldType: FieldMetadataType.MORPH_RELATION; morphId: string }
|
||||
);
|
||||
|
||||
const DEFAULT_DEFAULT_RELATION = {
|
||||
targetFieldName: (objectConfig: ObjectConfig) =>
|
||||
`target${capitalize(objectConfig.nameSingular)}`,
|
||||
targetLabel: (objectConfig: ObjectConfig) =>
|
||||
capitalize(objectConfig.nameSingular),
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
};
|
||||
|
||||
const DEFAULT_RELATION_CONFIGS = [
|
||||
{
|
||||
...DEFAULT_DEFAULT_RELATION,
|
||||
fieldName: 'timelineActivities',
|
||||
label: 'Timeline Activities',
|
||||
targetIcon: 'IconTimelineEvent',
|
||||
targetFieldType: FieldMetadataType.MORPH_RELATION,
|
||||
standardObjectKey: 'timelineActivity',
|
||||
morphId: STANDARD_OBJECTS.timelineActivity.morphIds.targetMorphId.morphId,
|
||||
},
|
||||
{
|
||||
...DEFAULT_DEFAULT_RELATION,
|
||||
fieldName: 'favorites',
|
||||
label: 'Favorites',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
targetIcon: 'IconHeart',
|
||||
targetFieldType: FieldMetadataType.RELATION,
|
||||
standardObjectKey: 'favorite',
|
||||
},
|
||||
{
|
||||
...DEFAULT_DEFAULT_RELATION,
|
||||
fieldName: 'attachments',
|
||||
label: 'Attachments',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
targetIcon: 'IconFileImport',
|
||||
targetFieldType: FieldMetadataType.MORPH_RELATION,
|
||||
standardObjectKey: 'attachment',
|
||||
morphId: STANDARD_OBJECTS.attachment.morphIds.targetMorphId.morphId,
|
||||
},
|
||||
{
|
||||
...DEFAULT_DEFAULT_RELATION,
|
||||
fieldName: 'noteTargets',
|
||||
label: 'Note Targets',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
targetIcon: 'IconCheckbox',
|
||||
targetFieldType: FieldMetadataType.MORPH_RELATION,
|
||||
standardObjectKey: 'noteTarget',
|
||||
morphId: STANDARD_OBJECTS.noteTarget.morphIds.targetMorphId.morphId,
|
||||
},
|
||||
{
|
||||
...DEFAULT_DEFAULT_RELATION,
|
||||
fieldName: 'taskTargets',
|
||||
label: 'Task Targets',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
targetIcon: 'IconCheckbox',
|
||||
targetFieldType: FieldMetadataType.MORPH_RELATION,
|
||||
standardObjectKey: 'taskTarget',
|
||||
morphId: STANDARD_OBJECTS.taskTarget.morphIds.targetMorphId.morphId,
|
||||
},
|
||||
] as const satisfies DefaultRelationConfig[];
|
||||
|
||||
const buildReverseField = ({
|
||||
config,
|
||||
forwardFieldUniversalIdentifier,
|
||||
objectConfig,
|
||||
universalIdentifier,
|
||||
}: {
|
||||
config: DefaultRelationConfig;
|
||||
objectConfig: ObjectConfig;
|
||||
universalIdentifier: string;
|
||||
forwardFieldUniversalIdentifier: string;
|
||||
}): FieldManifest<typeof config.targetFieldType> => {
|
||||
const standardObject = STANDARD_OBJECTS[config.standardObjectKey];
|
||||
|
||||
const commonFields = {
|
||||
name: config.targetFieldName(objectConfig),
|
||||
label: config.targetLabel(objectConfig),
|
||||
description: `${config.targetLabel(objectConfig)} ${objectConfig.labelSingular}`,
|
||||
icon: config.targetIcon,
|
||||
isNullable: true,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: `target${capitalize(objectConfig.nameSingular)}Id`,
|
||||
},
|
||||
universalIdentifier,
|
||||
objectUniversalIdentifier: standardObject.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
forwardFieldUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
objectConfig.universalIdentifier,
|
||||
};
|
||||
|
||||
if (config.targetFieldType === FieldMetadataType.MORPH_RELATION) {
|
||||
return {
|
||||
...commonFields,
|
||||
type: config.targetFieldType,
|
||||
morphId: config.morphId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...commonFields,
|
||||
type: config.targetFieldType,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefaultRelationObjectFields = (
|
||||
objectConfig: ObjectConfig,
|
||||
): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => {
|
||||
const objectFields: ObjectFieldManifest[] = [];
|
||||
const fields: FieldManifest[] = [];
|
||||
|
||||
for (const config of DEFAULT_RELATION_CONFIGS) {
|
||||
const standardObject = STANDARD_OBJECTS[config.standardObjectKey];
|
||||
|
||||
const forwardFieldUniversalIdentifier =
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: config.fieldName,
|
||||
});
|
||||
|
||||
const reverseFieldUniversalIdentifier =
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: `${config.fieldName}Inverse`,
|
||||
});
|
||||
|
||||
const forwardField: ObjectFieldManifest = {
|
||||
name: config.fieldName,
|
||||
label: config.label,
|
||||
description: `${objectConfig.labelPlural} tied to the ${config.targetLabel(objectConfig)}`,
|
||||
icon: config.icon,
|
||||
isNullable: false,
|
||||
type: FieldMetadataType.RELATION,
|
||||
universalSettings: { relationType: RelationType.ONE_TO_MANY },
|
||||
universalIdentifier: forwardFieldUniversalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
reverseFieldUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
standardObject.universalIdentifier,
|
||||
};
|
||||
|
||||
const reverseField = buildReverseField({
|
||||
config,
|
||||
objectConfig,
|
||||
universalIdentifier: reverseFieldUniversalIdentifier,
|
||||
forwardFieldUniversalIdentifier,
|
||||
});
|
||||
|
||||
objectFields.push(forwardField);
|
||||
fields.push(reverseField);
|
||||
}
|
||||
|
||||
return { objectFields, fields };
|
||||
};
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
|
||||
export const injectDefaultFieldsInObjectFields = (
|
||||
objectConfig: ObjectConfig,
|
||||
) => {
|
||||
const defaultObjectFields = getDefaultObjectFields(objectConfig);
|
||||
|
||||
const objectConfigFieldNames = (objectConfig.fields ?? []).map((f) => f.name);
|
||||
|
||||
const fieldsWithDefaultFields = [...objectConfig.fields];
|
||||
|
||||
for (const defaultField of defaultObjectFields) {
|
||||
if (!objectConfigFieldNames.includes(defaultField.name)) {
|
||||
fieldsWithDefaultFields.push(defaultField);
|
||||
}
|
||||
}
|
||||
|
||||
return fieldsWithDefaultFields;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { join } from 'path';
|
||||
import {
|
||||
DEFAULT_API_KEY_NAME,
|
||||
DEFAULT_API_URL_NAME,
|
||||
GENERATED_DIR,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
export class ClientService {
|
||||
@@ -54,7 +55,7 @@ export class ClientService {
|
||||
}
|
||||
|
||||
private resolveGeneratedPath(appPath: string): string {
|
||||
return join(appPath, 'node_modules', 'twenty-sdk', 'generated');
|
||||
return join(appPath, 'node_modules', 'twenty-sdk', GENERATED_DIR);
|
||||
}
|
||||
|
||||
private async injectTwentyClient(output: string) {
|
||||
|
||||
+9
-5
@@ -6,7 +6,11 @@ import { FileUploader } from '@/cli/utilities/file/file-uploader';
|
||||
import crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { OUTPUT_DIR } from 'twenty-shared/application';
|
||||
import {
|
||||
OUTPUT_DIR,
|
||||
GENERATED_DIR,
|
||||
API_CLIENT_DIR,
|
||||
} from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
const API_CLIENT_FILES = ['types.ts', 'schema.ts'];
|
||||
@@ -114,14 +118,14 @@ export class UploadFilesOrchestratorStep {
|
||||
appPath,
|
||||
'node_modules',
|
||||
'twenty-sdk',
|
||||
'generated',
|
||||
GENERATED_DIR,
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(generatedDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outputDir = join(appPath, OUTPUT_DIR, 'api-client');
|
||||
const outputDir = join(appPath, OUTPUT_DIR, API_CLIENT_DIR);
|
||||
|
||||
await fs.ensureDir(outputDir);
|
||||
|
||||
@@ -137,8 +141,8 @@ export class UploadFilesOrchestratorStep {
|
||||
const content = await fs.readFile(absoluteSourcePath);
|
||||
const checksum = crypto.createHash('md5').update(content).digest('hex');
|
||||
|
||||
const builtPath = join(OUTPUT_DIR, 'api-client', fileName);
|
||||
const sourcePath = join('api-client', fileName);
|
||||
const builtPath = join(OUTPUT_DIR, API_CLIENT_DIR, fileName);
|
||||
const sourcePath = join(API_CLIENT_DIR, fileName);
|
||||
|
||||
this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, {
|
||||
checksum,
|
||||
|
||||
Reference in New Issue
Block a user