mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
[TYPES] UniversalEntity JsonbProperty and SerializedRelation (#17396)
# Introduction
In this PR we're introducing mainly two branded type signatures for both
`JsonbProperty` entities properties and `SerializedRelation` (jsonb
serialized property storing another entity id).
Allowing to dynamically map over them later in order to build universal
`jsonb` `serialized` relations.
## `JsonbProperty`
A branded wrapper type that marks entity properties stored as PostgreSQL
JSONB columns. It adds a phantom brand `__JsonbPropertyBrand__` to
object types while leaving primitives unchanged. The branded key is
optional and typed as never, also omitted when transpiled to
`UniversalFlat`
**Should be used at entities lvl only:**
```typescript
@Column({ type: 'jsonb', nullable: false })
gridPosition: JsonbProperty<GridPosition>;
@Column({ nullable: false, type: 'jsonb', default: [] })
publishedVersions: JsonbProperty<string[]>;
```
## `SerializedRelation`
A branded string type that marks foreign key IDs stored inside JSONB
objects. These are entity references serialized within a JSONB column
rather than being a regular database foreign key.
**Usage in jsonb property generic***
```ts
type FieldMetadataRelationSettings = {
relationType: RelationType;
onDelete?: RelationOnDeleteAction;
joinColumnName?: string | null;
junctionTargetFieldId?: SerializedRelation;
};
```
## `FormatJsonbSerializedRelation<T>`
A transformation type that processes JSONB properties for universal
entity mapping. It:
1. Detects properties with the `JsonbProperty` brand
2. Finds `SerializedRelation` properties
3. Renames them from `*Id` to `*UniversalIdentifier`
4. Removes the brand from the output type ( optional though )
```typescript
// Input: JsonbProperty<{ targetFieldMetadataId: SerializedRelation }>
// Output: { targetFieldMetadataUniversalIdentifier: SerializedRelation }
```
## Result
An example of the dynamic type mapping, through a type-test example
```ts
type SettingsTestCase = UniversalFlatFieldMetadata<
| FieldMetadataType.RELATION
| FieldMetadataType.NUMBER
| FieldMetadataType.TEXT
>['settings']
type SettingsExpectedResult =
| {
relationType: RelationType;
onDelete?: RelationOnDeleteAction | undefined;
joinColumnName?: string | null | undefined;
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
}
| {
dataType?: NumberDataType | undefined;
decimals?: number | undefined;
type?: FieldNumberVariant | undefined;
}
| {
displayedMaxRows?: number | undefined;
}
| null;
type Assertions = [
Expect<Equal<SettingsTestCase, SettingsExpectedResult>>,
]
```
## Remarks
- Removed duplicated twenty-server and twenty-shared typed
- Removed class validator instances for default value that were not used
at runtime, we will refactor that to add validation across all entities
following a same pattern
This commit is contained in:
parent
d0bc9a94c0
commit
44202668fd
62 changed files with 1082 additions and 1070 deletions
|
|
@ -27,6 +27,7 @@ Examples of existing syncable entities: `skill`, `agent`, `view`, `viewField`, `
|
|||
3. [Step-by-Step Implementation](#step-by-step-implementation)
|
||||
- [Step 1: Add Metadata Name Constant](#step-1-add-metadata-name-constant-twenty-shared)
|
||||
- [Step 2: Create TypeORM Entity](#step-2-create-typeorm-entity)
|
||||
- [Step 2b: Using JsonbProperty and SerializedRelation Types](#step-2b-using-jsonbproperty-and-serializedrelation-types)
|
||||
- [Step 3: Define Flat Entity Type](#step-3-define-flat-entity-type)
|
||||
- [Step 4: Define Editable Properties](#step-4-define-editable-properties)
|
||||
- [Step 5: Register in Central Constants](#step-5-register-in-central-constants)
|
||||
|
|
@ -237,6 +238,160 @@ export abstract class WorkspaceRelatedEntity {
|
|||
|
||||
---
|
||||
|
||||
### Step 2b: Using JsonbProperty and SerializedRelation Types
|
||||
|
||||
When your entity has JSONB columns or stores foreign key references inside JSONB structures, you must use the branded type wrappers to enable automatic universal identifier mapping.
|
||||
|
||||
#### JsonbProperty Wrapper
|
||||
|
||||
Wrap all JSONB column types with `JsonbProperty<T>` to mark them for the universal entity transformation system:
|
||||
|
||||
```typescript
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
@Entity('myEntity')
|
||||
export class MyEntityEntity extends SyncableEntity {
|
||||
// Simple JSONB column - wrap the type
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
settings: JsonbProperty<MyEntitySettings> | null;
|
||||
|
||||
// JSONB column with complex type
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
configuration: JsonbProperty<MyEntityConfiguration>;
|
||||
|
||||
// Array stored as JSONB
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
tags: JsonbProperty<string[]> | null;
|
||||
}
|
||||
```
|
||||
|
||||
**When to use `JsonbProperty<T>`:**
|
||||
- Any column with `type: 'jsonb'` that stores an object or array
|
||||
- Configuration objects, settings, metadata blobs
|
||||
- Any structured data stored as JSON in the database
|
||||
|
||||
**What it enables:**
|
||||
- The type system can identify which properties are JSONB columns
|
||||
- Automatic transformation of serialized relations within JSONB structures
|
||||
- Type-safe universal entity mapping
|
||||
|
||||
#### SerializedRelation Type
|
||||
|
||||
Use `SerializedRelation` for properties **inside JSONB structures** that store foreign key references (entity IDs):
|
||||
|
||||
```typescript
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
// Define the JSONB structure type
|
||||
type MyEntityConfiguration = {
|
||||
name: string;
|
||||
// This stores a reference to another field's ID - use SerializedRelation
|
||||
targetFieldMetadataId: SerializedRelation;
|
||||
// This stores a reference to an object's ID
|
||||
sourceObjectMetadataId: SerializedRelation;
|
||||
// Regular string - NOT a foreign key reference
|
||||
displayFormat: string;
|
||||
};
|
||||
|
||||
@Entity('myEntity')
|
||||
export class MyEntityEntity extends SyncableEntity {
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
configuration: JsonbProperty<MyEntityConfiguration>;
|
||||
}
|
||||
```
|
||||
|
||||
**When to use `SerializedRelation`:**
|
||||
- Properties inside JSONB that store UUIDs referencing other entities
|
||||
- Foreign key relationships that can't use TypeORM relations (because they're in JSONB)
|
||||
- Any `*Id` property inside a JSONB structure that references another metadata entity
|
||||
|
||||
**What it enables:**
|
||||
- Automatic renaming from `*Id` to `*UniversalIdentifier` in universal entities
|
||||
- Type-safe extraction of serialized relation properties
|
||||
- Proper handling during workspace sync/migration
|
||||
|
||||
#### Complete Example
|
||||
|
||||
```typescript
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
// JSONB structure with serialized relations
|
||||
type WidgetConfiguration = {
|
||||
title: string;
|
||||
// Foreign keys stored in JSONB - use SerializedRelation
|
||||
fieldMetadataId: SerializedRelation;
|
||||
objectMetadataId: SerializedRelation;
|
||||
// Optional foreign key
|
||||
viewId?: SerializedRelation;
|
||||
// Regular properties (not foreign keys)
|
||||
displayMode: 'compact' | 'expanded';
|
||||
maxItems: number;
|
||||
};
|
||||
|
||||
type GridPosition = {
|
||||
row: number;
|
||||
column: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
@Entity('widget')
|
||||
export class WidgetEntity extends SyncableEntity implements Required<WidgetEntity> {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
standardId: string | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
// JSONB column with serialized relations - wrap with JsonbProperty
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
configuration: JsonbProperty<WidgetConfiguration>;
|
||||
|
||||
// JSONB column without serialized relations - still wrap with JsonbProperty
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
gridPosition: JsonbProperty<GridPosition>;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
||||
// ... other columns
|
||||
}
|
||||
```
|
||||
|
||||
**Result in Universal Entity:**
|
||||
|
||||
When transformed to a universal entity, the `configuration` property will have its `SerializedRelation` fields automatically renamed:
|
||||
|
||||
```typescript
|
||||
// Original (in database/flat entity)
|
||||
{
|
||||
fieldMetadataId: "abc-123",
|
||||
objectMetadataId: "def-456",
|
||||
viewId: "ghi-789",
|
||||
displayMode: "compact",
|
||||
maxItems: 10,
|
||||
}
|
||||
|
||||
// Transformed (in universal entity)
|
||||
{
|
||||
fieldMetadataUniversalIdentifier: "abc-123",
|
||||
objectMetadataUniversalIdentifier: "def-456",
|
||||
viewUniversalIdentifier: "ghi-789",
|
||||
displayMode: "compact",
|
||||
maxItems: 10,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Define Flat Entity Type
|
||||
|
||||
**File:** `src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type.ts`
|
||||
|
|
@ -1143,6 +1298,11 @@ Before considering your syncable entity complete, verify:
|
|||
- [ ] Entity has `isCustom` boolean column
|
||||
- [ ] Entity-to-flat transform sets `universalIdentifier` correctly (`standardId || id`)
|
||||
|
||||
### JSONB Properties and Serialized Relations
|
||||
- [ ] All JSONB columns are wrapped with `JsonbProperty<T>`
|
||||
- [ ] Foreign key references inside JSONB structures use `SerializedRelation` type
|
||||
- [ ] JSONB structure types are properly defined with `SerializedRelation` for `*Id` properties
|
||||
|
||||
### Registration (twenty-shared)
|
||||
- [ ] Metadata name added to `ALL_METADATA_NAME`
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
FieldMetadataRelationSettings,
|
||||
FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'twenty-shared/types';
|
||||
|
|
@ -106,7 +106,7 @@ export class UpdateTaskOnDeleteActionCommand extends ActiveOrSuspendedWorkspaces
|
|||
}
|
||||
|
||||
const taskFieldSettings =
|
||||
taskField.settings as FieldMetadataRelationSettings;
|
||||
taskField.settings as FieldMetadataSettingsMapping['RELATION'];
|
||||
|
||||
if (taskFieldSettings?.onDelete === RelationOnDeleteAction.CASCADE) {
|
||||
this.logger.log(
|
||||
|
|
@ -121,7 +121,7 @@ export class UpdateTaskOnDeleteActionCommand extends ActiveOrSuspendedWorkspaces
|
|||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
const updatedSettings: FieldMetadataRelationSettings = {
|
||||
const updatedSettings: FieldMetadataSettingsMapping['RELATION'] = {
|
||||
...taskFieldSettings,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { msg } from '@lingui/core/macro';
|
||||
import { isNull, isUndefined } from '@sniptt/guards';
|
||||
import {
|
||||
FieldMetadataFilesSettings,
|
||||
FieldMetadataRelationSettings,
|
||||
FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
ObjectRecord,
|
||||
RelationType,
|
||||
|
|
@ -219,7 +218,7 @@ export class DataArgProcessor {
|
|||
case FieldMetadataType.RELATION:
|
||||
case FieldMetadataType.MORPH_RELATION: {
|
||||
const fieldMetadataRelationSettings =
|
||||
fieldMetadata.settings as FieldMetadataRelationSettings;
|
||||
fieldMetadata.settings as FieldMetadataSettingsMapping['RELATION'];
|
||||
|
||||
if (
|
||||
fieldMetadataRelationSettings.relationType ===
|
||||
|
|
@ -252,7 +251,7 @@ export class DataArgProcessor {
|
|||
const validatedValue = validateFilesFieldOrThrow(
|
||||
value,
|
||||
key,
|
||||
fieldMetadata.settings as FieldMetadataFilesSettings,
|
||||
fieldMetadata.settings as FieldMetadataSettingsMapping['FILES'],
|
||||
);
|
||||
|
||||
return transformRawJsonField(validatedValue);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { inspect } from 'util';
|
|||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { type FieldMetadataFilesSettings } from 'twenty-shared/types';
|
||||
import { z } from 'zod';
|
||||
import { type FieldMetadataSettingsMapping } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
CommonQueryRunnerException,
|
||||
|
|
@ -24,7 +24,7 @@ export type FileItem = z.infer<typeof fileItemSchema>;
|
|||
export const validateFilesFieldOrThrow = (
|
||||
value: unknown,
|
||||
fieldName: string,
|
||||
settings: FieldMetadataFilesSettings,
|
||||
settings: FieldMetadataSettingsMapping['FILES'],
|
||||
): FileItem[] | null => {
|
||||
if (isNull(value)) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
QUERY_MAX_RECORDS_FROM_RELATION,
|
||||
} from 'twenty-shared/constants';
|
||||
import {
|
||||
FieldMetadataRelationSettings,
|
||||
FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
ObjectRecord,
|
||||
RelationType,
|
||||
|
|
@ -256,8 +256,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
|
|||
|
||||
const relationType =
|
||||
isDryRun && fieldMetadata.type === FieldMetadataType.RELATION
|
||||
? (fieldMetadata.settings as FieldMetadataRelationSettings)
|
||||
?.relationType
|
||||
? (
|
||||
fieldMetadata.settings as FieldMetadataSettingsMapping['RELATION']
|
||||
)?.relationType
|
||||
: undefined;
|
||||
|
||||
mergedResult[fieldName] = mergeFieldValues(
|
||||
|
|
@ -376,7 +377,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
|
|||
}
|
||||
|
||||
const relationSettings = field.settings as
|
||||
| FieldMetadataRelationSettings
|
||||
| FieldMetadataSettingsMapping['RELATION']
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { type GraphQLScalarType } from 'graphql';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import {
|
||||
type FieldMetadataType,
|
||||
type FieldMetadataDefaultValue,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,10 @@ import {
|
|||
type FieldMetadataSettings,
|
||||
FieldMetadataType,
|
||||
NumberDataType,
|
||||
type FieldMetadataDefaultValue,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ describe('computeSchemaComponents', () => {
|
|||
"description": "Object description",
|
||||
"example": {
|
||||
"fieldCurrency": {
|
||||
"amountMicros": 284000000,
|
||||
"amountMicros": "284000000",
|
||||
"currencyCode": "EUR",
|
||||
},
|
||||
"fieldEmails": {
|
||||
|
|
@ -554,7 +554,7 @@ describe('computeSchemaComponents', () => {
|
|||
"description": "Object description",
|
||||
"example": {
|
||||
"fieldCurrency": {
|
||||
"amountMicros": 253000000,
|
||||
"amountMicros": "253000000",
|
||||
"currencyCode": "EUR",
|
||||
},
|
||||
"fieldEmails": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { type OpenAPIV3_1 } from 'openapi-types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
type FieldMetadataDefaultValue,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { generateRandomFieldValue } from 'src/engine/core-modules/open-api/utils/generate-random-field-value.util';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
type FieldMetadataDefaultValue,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ export const generateRandomFieldValue = ({
|
|||
|
||||
case FieldMetadataType.CURRENCY: {
|
||||
return {
|
||||
amountMicros: faker.number.int({ min: 100, max: 1_000 }) * 1_000_000,
|
||||
amountMicros: `${faker.number.int({ min: 100, max: 1_000 }) * 1_000_000}`,
|
||||
currencyCode: 'EUR',
|
||||
};
|
||||
}
|
||||
|
|
@ -131,7 +132,6 @@ export const generateRandomFieldValue = ({
|
|||
case FieldMetadataType.ACTOR: {
|
||||
return {
|
||||
source: 'MANUAL',
|
||||
context: {},
|
||||
name: faker.person.fullName(),
|
||||
workspaceMemberId: null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
ModelId,
|
||||
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
@Entity('agent')
|
||||
@Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt'])
|
||||
|
|
@ -52,7 +53,7 @@ export class AgentEntity
|
|||
|
||||
// Should not be nullable
|
||||
@Column({ nullable: true, type: 'jsonb', default: { type: 'text' } })
|
||||
responseFormat: AgentResponseFormat;
|
||||
responseFormat: JsonbProperty<AgentResponseFormat>;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
|
@ -67,7 +68,7 @@ export class AgentEntity
|
|||
deletedAt: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
modelConfiguration: ModelConfiguration | null;
|
||||
modelConfiguration: JsonbProperty<ModelConfiguration> | null;
|
||||
|
||||
@Column({ type: 'text', array: true, default: '{}' })
|
||||
evaluationInputs: string[];
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ export class CronTriggerEntity
|
|||
id: string;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb' })
|
||||
settings: CronTriggerSettings;
|
||||
settings: JsonbProperty<CronTriggerSettings>;
|
||||
|
||||
@ManyToOne(
|
||||
() => ServerlessFunctionEntity,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ export class DatabaseEventTriggerEntity
|
|||
id: string;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb' })
|
||||
settings: DatabaseEventTriggerSettings;
|
||||
settings: JsonbProperty<DatabaseEventTriggerSettings>;
|
||||
|
||||
@ManyToOne(
|
||||
() => ServerlessFunctionEntity,
|
||||
|
|
|
|||
|
|
@ -1,220 +0,0 @@
|
|||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsNumberString,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Matches,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
|
||||
import { IsQuotedString } from 'src/engine/metadata-modules/field-metadata/validators/is-quoted-string.validator';
|
||||
|
||||
export const fieldMetadataDefaultValueFunctionName = {
|
||||
UUID: 'uuid',
|
||||
NOW: 'now',
|
||||
} as const;
|
||||
|
||||
export type FieldMetadataDefaultValueFunctionNames =
|
||||
(typeof fieldMetadataDefaultValueFunctionName)[keyof typeof fieldMetadataDefaultValueFunctionName];
|
||||
|
||||
export class FieldMetadataDefaultValueString {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueRawJson {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsObject() // TODO: Should this also allow arrays?
|
||||
value: object | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueRichTextV2 {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
blocknote: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
markdown: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueRichText {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueNumber {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumber()
|
||||
value: number | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueBoolean {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsBoolean()
|
||||
value: boolean | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueStringArray {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsArray()
|
||||
@IsQuotedString({ each: true })
|
||||
value: string[] | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueDateTime {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsDate()
|
||||
value: Date | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueDate {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsDate()
|
||||
value: Date | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueCurrency {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumberString()
|
||||
amountMicros: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
currencyCode: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueFullName {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
firstName: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
lastName: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueUuidFunction {
|
||||
@Matches(fieldMetadataDefaultValueFunctionName.UUID)
|
||||
@IsNotEmpty()
|
||||
value: typeof fieldMetadataDefaultValueFunctionName.UUID;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueNowFunction {
|
||||
@Matches(fieldMetadataDefaultValueFunctionName.NOW)
|
||||
@IsNotEmpty()
|
||||
value: typeof fieldMetadataDefaultValueFunctionName.NOW;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueAddress {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressStreet1: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressStreet2: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressCity: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressPostcode: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressState: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressCountry: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumber()
|
||||
addressLat: number | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumber()
|
||||
addressLng: number | null;
|
||||
}
|
||||
|
||||
class LinkMetadata {
|
||||
@IsString()
|
||||
label: string;
|
||||
|
||||
@IsString()
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueLinks {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryLinkLabel: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryLinkUrl: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsArray()
|
||||
secondaryLinks: LinkMetadata[] | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultActor {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
source: string;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
workspaceMemberId?: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueEmails {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryEmail: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsObject()
|
||||
additionalEmails: object | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValuePhones {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryPhoneNumber: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryPhoneCountryCode: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryPhoneCallingCode: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsObject()
|
||||
additionalPhones: object | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultArray {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsArray()
|
||||
value: string[] | null;
|
||||
}
|
||||
|
|
@ -27,10 +27,9 @@ import {
|
|||
type FieldMetadataOptions,
|
||||
type FieldMetadataSettings,
|
||||
FieldMetadataType,
|
||||
type FieldMetadataDefaultValue,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
FieldMetadataDefaultValue,
|
||||
FieldMetadataOptions,
|
||||
FieldMetadataSettings,
|
||||
FieldMetadataType,
|
||||
|
|
@ -19,8 +20,6 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { type FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
|
||||
import { AssignIfIsGivenFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/assign-if-is-given-field-metadata-type.type';
|
||||
import { AssignTypeIfIsMorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/assign-type-if-is-morph-or-relation-field-metadata-type.type';
|
||||
|
|
@ -31,6 +30,7 @@ import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities
|
|||
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
// This entity is used as a reference test case for type utilities in:
|
||||
// Modifying relations or properties may require updating type test expectations for Typecheck to pass.
|
||||
|
|
@ -91,7 +91,7 @@ export class FieldMetadataEntity<
|
|||
label: string;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
defaultValue: FieldMetadataDefaultValue<TFieldMetadataType>;
|
||||
defaultValue: JsonbProperty<FieldMetadataDefaultValue<TFieldMetadataType>>;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string | null;
|
||||
|
|
@ -100,13 +100,13 @@ export class FieldMetadataEntity<
|
|||
icon: string | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
standardOverrides: FieldStandardOverridesDTO | null;
|
||||
standardOverrides: JsonbProperty<FieldStandardOverridesDTO> | null;
|
||||
|
||||
@Column('jsonb', { nullable: true })
|
||||
options: FieldMetadataOptions<TFieldMetadataType>;
|
||||
options: JsonbProperty<FieldMetadataOptions<TFieldMetadataType>>;
|
||||
|
||||
@Column('jsonb', { nullable: true })
|
||||
settings: FieldMetadataSettings<TFieldMetadataType>;
|
||||
settings: JsonbProperty<FieldMetadataSettings<TFieldMetadataType>>;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
import { type FieldMetadataType, type IsExactly } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
type FieldMetadataDefaultActor,
|
||||
type FieldMetadataDefaultArray,
|
||||
type FieldMetadataDefaultValueAddress,
|
||||
type FieldMetadataDefaultValueBoolean,
|
||||
type FieldMetadataDefaultValueCurrency,
|
||||
type FieldMetadataDefaultValueDateTime,
|
||||
type FieldMetadataDefaultValueEmails,
|
||||
type FieldMetadataDefaultValueFullName,
|
||||
type FieldMetadataDefaultValueLinks,
|
||||
type FieldMetadataDefaultValueNowFunction,
|
||||
type FieldMetadataDefaultValueNumber,
|
||||
type FieldMetadataDefaultValuePhones,
|
||||
type FieldMetadataDefaultValueRawJson,
|
||||
type FieldMetadataDefaultValueRichText,
|
||||
type FieldMetadataDefaultValueString,
|
||||
type FieldMetadataDefaultValueStringArray,
|
||||
type FieldMetadataDefaultValueUuidFunction,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
|
||||
type ExtractValueType<T> = T extends { value: infer V } ? V : T;
|
||||
|
||||
type UnionOfValues<T> = T[keyof T];
|
||||
|
||||
type FieldMetadataDefaultValueMapping = {
|
||||
[FieldMetadataType.UUID]:
|
||||
| FieldMetadataDefaultValueString
|
||||
| FieldMetadataDefaultValueUuidFunction;
|
||||
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.PHONES]: FieldMetadataDefaultValuePhones;
|
||||
[FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails;
|
||||
[FieldMetadataType.DATE_TIME]:
|
||||
| FieldMetadataDefaultValueDateTime
|
||||
| FieldMetadataDefaultValueNowFunction;
|
||||
[FieldMetadataType.DATE]:
|
||||
| FieldMetadataDefaultValueDateTime
|
||||
| FieldMetadataDefaultValueNowFunction;
|
||||
[FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean;
|
||||
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
|
||||
[FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber;
|
||||
[FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.LINKS]: FieldMetadataDefaultValueLinks;
|
||||
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency;
|
||||
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName;
|
||||
[FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress;
|
||||
[FieldMetadataType.RATING]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.SELECT]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray;
|
||||
[FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
|
||||
[FieldMetadataType.RICH_TEXT]: FieldMetadataDefaultValueRichText;
|
||||
[FieldMetadataType.ACTOR]: FieldMetadataDefaultActor;
|
||||
[FieldMetadataType.ARRAY]: FieldMetadataDefaultArray;
|
||||
};
|
||||
|
||||
export type FieldMetadataClassValidation =
|
||||
UnionOfValues<FieldMetadataDefaultValueMapping>;
|
||||
|
||||
export type FieldMetadataFunctionDefaultValue = ExtractValueType<
|
||||
FieldMetadataDefaultValueUuidFunction | FieldMetadataDefaultValueNowFunction
|
||||
>;
|
||||
|
||||
export type FieldMetadataDefaultValueForType<
|
||||
T extends keyof FieldMetadataDefaultValueMapping,
|
||||
> = ExtractValueType<FieldMetadataDefaultValueMapping[T]> | null;
|
||||
|
||||
export type FieldMetadataDefaultValueForAnyType = ExtractValueType<
|
||||
UnionOfValues<FieldMetadataDefaultValueMapping>
|
||||
> | null;
|
||||
|
||||
export type FieldMetadataDefaultValue<
|
||||
T extends FieldMetadataType = FieldMetadataType,
|
||||
> =
|
||||
IsExactly<T, FieldMetadataType> extends true
|
||||
? FieldMetadataDefaultValueForAnyType | null // Could be improved to be | unknown
|
||||
: T extends keyof FieldMetadataDefaultValueMapping
|
||||
? FieldMetadataDefaultValueForType<T>
|
||||
: never | null;
|
||||
|
||||
type FieldMetadataDefaultValueExtractedTypes = {
|
||||
[K in keyof FieldMetadataDefaultValueMapping]: ExtractValueType<
|
||||
FieldMetadataDefaultValueMapping[K]
|
||||
>;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultSerializableValue =
|
||||
| FieldMetadataDefaultValueExtractedTypes[keyof FieldMetadataDefaultValueExtractedTypes]
|
||||
| null;
|
||||
|
|
@ -1,22 +1,16 @@
|
|||
import { type Expect, type HasAllProperties } from 'twenty-shared/testing';
|
||||
import {
|
||||
type FieldMetadataMultiItemSettings,
|
||||
type AllFieldMetadataSettings,
|
||||
type FieldMetadataDefaultValueForAnyType,
|
||||
type FieldMetadataDefaultValueMapping,
|
||||
type FieldMetadataOptionForAnyType,
|
||||
type FieldMetadataSettingsMapping,
|
||||
type FieldMetadataType,
|
||||
type NullablePartial,
|
||||
type AllFieldMetadataSettings,
|
||||
type FieldMetadataDateSettings,
|
||||
type FieldMetadataDateTimeSettings,
|
||||
type FieldMetadataNumberSettings,
|
||||
type FieldMetadataRelationSettings,
|
||||
type FieldMetadataTextSettings,
|
||||
} from 'twenty-shared/types';
|
||||
import { type Relation as TypeOrmRelation } from 'typeorm';
|
||||
|
||||
import {
|
||||
type FieldMetadataDefaultValueForAnyType,
|
||||
type FieldMetadataDefaultValueForType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultOption,
|
||||
|
|
@ -138,69 +132,109 @@ type SettingsAssertions = [
|
|||
Expect<
|
||||
HasAllProperties<
|
||||
TextFieldMetadata,
|
||||
{ settings: FieldMetadataTextSettings | null }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.TEXT]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
NumberFieldMetadata,
|
||||
{ settings: FieldMetadataNumberSettings | null }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.NUMBER]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
DateFieldMetadata,
|
||||
{ settings: FieldMetadataDateSettings | null }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.DATE]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
DateTimeFieldMetadata,
|
||||
{ settings: FieldMetadataDateTimeSettings | null }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.DATE_TIME]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
ArrayFieldMetadata,
|
||||
{ settings: FieldMetadataMultiItemSettings | null }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.ARRAY]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
PhonesFieldMetadata,
|
||||
{ settings: FieldMetadataMultiItemSettings | null }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.PHONES]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
EmailsFieldMetadata,
|
||||
{ settings: FieldMetadataMultiItemSettings | null }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.EMAILS]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
LinksFieldMetadata,
|
||||
{ settings: FieldMetadataMultiItemSettings | null }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.LINKS]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
RelationFieldMetadata,
|
||||
{ settings: FieldMetadataRelationSettings }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.RELATION]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
MorphRelationFieldMetadata,
|
||||
{ settings: FieldMetadataRelationSettings }
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.MORPH_RELATION]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
AbstractFieldMetadata,
|
||||
{ settings: AllFieldMetadataSettings | null }
|
||||
{ settings: JsonbProperty<AllFieldMetadataSettings> | null }
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
|
@ -210,20 +244,30 @@ type DefaultValueAssertions = [
|
|||
Expect<
|
||||
HasAllProperties<
|
||||
UUIDFieldMetadata,
|
||||
{ defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.UUID> }
|
||||
{
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.UUID]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
TextFieldMetadata,
|
||||
{ defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.TEXT> }
|
||||
{
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.TEXT]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
NumberFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.NUMBER>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.NUMBER]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -231,21 +275,29 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
BooleanFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.BOOLEAN>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.BOOLEAN]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
DateFieldMetadata,
|
||||
{ defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.DATE> }
|
||||
{
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.DATE]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
DateTimeFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.DATE_TIME>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.DATE_TIME]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -253,7 +305,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
CurrencyFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.CURRENCY>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.CURRENCY]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -261,7 +315,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
FullNameFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.FULL_NAME>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.FULL_NAME]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -269,7 +325,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
RatingFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.RATING>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.RATING]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -277,7 +335,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
SelectFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.SELECT>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.SELECT]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -285,7 +345,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
MultiSelectFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.MULTI_SELECT>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.MULTI_SELECT]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -293,7 +355,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
PositionFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.POSITION>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.POSITION]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -301,7 +365,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
RawJsonFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.RAW_JSON>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.RAW_JSON]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -309,7 +375,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
RichTextFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.RICH_TEXT>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.RICH_TEXT]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -317,7 +385,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
ActorFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.ACTOR>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.ACTOR]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -325,7 +395,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
ArrayFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.ARRAY>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.ARRAY]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -333,7 +405,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
PhonesFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.PHONES>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.PHONES]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -341,7 +415,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
EmailsFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.EMAILS>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.EMAILS]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -349,7 +425,9 @@ type DefaultValueAssertions = [
|
|||
HasAllProperties<
|
||||
LinksFieldMetadata,
|
||||
{
|
||||
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.LINKS>;
|
||||
defaultValue: JsonbProperty<
|
||||
FieldMetadataDefaultValueMapping[FieldMetadataType.LINKS]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
@ -364,7 +442,9 @@ type DefaultValueAssertions = [
|
|||
Expect<
|
||||
HasAllProperties<
|
||||
AbstractFieldMetadata,
|
||||
{ defaultValue: FieldMetadataDefaultValueForAnyType | null }
|
||||
{
|
||||
defaultValue: JsonbProperty<FieldMetadataDefaultValueForAnyType | null>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
|
@ -378,19 +458,19 @@ type OptionsAssertions = [
|
|||
Expect<
|
||||
HasAllProperties<
|
||||
RatingFieldMetadata,
|
||||
{ options: FieldMetadataDefaultOption[] }
|
||||
{ options: JsonbProperty<FieldMetadataDefaultOption[]> }
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
SelectFieldMetadata,
|
||||
{ options: FieldMetadataComplexOption[] }
|
||||
{ options: JsonbProperty<FieldMetadataComplexOption[]> }
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
MultiSelectFieldMetadata,
|
||||
{ options: FieldMetadataComplexOption[] }
|
||||
{ options: JsonbProperty<FieldMetadataComplexOption[]> }
|
||||
>
|
||||
>,
|
||||
|
||||
|
|
@ -417,9 +497,7 @@ type OptionsAssertions = [
|
|||
HasAllProperties<
|
||||
AbstractFieldMetadata,
|
||||
{
|
||||
options:
|
||||
| null
|
||||
| (FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]);
|
||||
options: JsonbProperty<FieldMetadataOptionForAnyType>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util';
|
||||
|
||||
describe('validateDefaultValueForType', () => {
|
||||
it('should return true for null defaultValue', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.TEXT, null).isValid,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Dynamic default values
|
||||
it('should validate uuid dynamic default value for UUID type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.UUID, 'uuid').isValid,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate now dynamic default value for DATE_TIME type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.DATE_TIME, 'now').isValid,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for mismatched dynamic default value', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.UUID, 'now').isValid,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// Static default values
|
||||
it('should validate string default value for TEXT type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.TEXT, "'test'").isValid,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid string default value for TEXT type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.TEXT, 123).isValid,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate number default value for NUMBER type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.NUMBER, 100).isValid,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid number default value for NUMBER type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.NUMBER, '100').isValid,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate boolean default value for BOOLEAN type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.BOOLEAN, true).isValid,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid boolean default value for BOOLEAN type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.BOOLEAN, 'true').isValid,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// CURRENCY type
|
||||
it('should validate CURRENCY default value', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(FieldMetadataType.CURRENCY, {
|
||||
amountMicros: '100',
|
||||
currencyCode: "'USD'",
|
||||
}).isValid,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid CURRENCY default value', () => {
|
||||
expect(
|
||||
validateDefaultValueForType(
|
||||
// @ts-expect-error Just for testing purposes
|
||||
{ amountMicros: 100, currencyCode: "'USD'" },
|
||||
FieldMetadataType.CURRENCY,
|
||||
).isValid,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// Unknown type
|
||||
it('should return false for unknown type', () => {
|
||||
expect(
|
||||
validateDefaultValueForType('unknown' as FieldMetadataType, "'test'")
|
||||
.isValid,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { FieldActorSource, FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import {
|
||||
FieldActorSource,
|
||||
type FieldMetadataDefaultValue,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
// No need to refactor as unused in workspace migration v2
|
||||
export function generateDefaultValue(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import {
|
||||
type FieldMetadataDefaultSerializableValue,
|
||||
type FieldMetadataFunctionDefaultValue,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import {
|
||||
type FieldMetadataDefaultValueForAnyType,
|
||||
type FieldMetadataDefaultValueFunctionNames,
|
||||
type FieldMetadataFunctionDefaultValue,
|
||||
fieldMetadataDefaultValueFunctionName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const isFunctionDefaultValue = (
|
||||
defaultValue: FieldMetadataDefaultSerializableValue,
|
||||
defaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
): defaultValue is FieldMetadataFunctionDefaultValue => {
|
||||
return (
|
||||
typeof defaultValue === 'string' &&
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
FieldMetadataException,
|
||||
|
|
@ -8,7 +8,7 @@ import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metada
|
|||
import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util';
|
||||
|
||||
export const serializeDefaultValue = (
|
||||
defaultValue?: FieldMetadataDefaultSerializableValue,
|
||||
defaultValue?: FieldMetadataDefaultValueForAnyType,
|
||||
) => {
|
||||
if (defaultValue === undefined || defaultValue === null) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type FieldMetadataFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { type FieldMetadataFunctionDefaultValue } from 'twenty-shared/types';
|
||||
|
||||
export const serializeFunctionDefaultValue = (
|
||||
defaultValue?: FieldMetadataFunctionDefaultValue,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { type FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
|
||||
export const unserializeDefaultValue = (
|
||||
serializedDefaultValue: FieldMetadataDefaultSerializableValue,
|
||||
serializedDefaultValue: FieldMetadataDefaultValueForAnyType,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): any => {
|
||||
if (serializedDefaultValue === undefined || serializedDefaultValue === null) {
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
import { plainToInstance } from 'class-transformer';
|
||||
import { type ValidationError, validateSync } from 'class-validator';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
type FieldMetadataClassValidation,
|
||||
type FieldMetadataDefaultValue,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import {
|
||||
FieldMetadataDefaultActor,
|
||||
FieldMetadataDefaultValueAddress,
|
||||
FieldMetadataDefaultValueBoolean,
|
||||
FieldMetadataDefaultValueCurrency,
|
||||
FieldMetadataDefaultValueDate,
|
||||
FieldMetadataDefaultValueDateTime,
|
||||
FieldMetadataDefaultValueEmails,
|
||||
FieldMetadataDefaultValueFullName,
|
||||
FieldMetadataDefaultValueLinks,
|
||||
FieldMetadataDefaultValueNowFunction,
|
||||
FieldMetadataDefaultValueNumber,
|
||||
FieldMetadataDefaultValuePhones,
|
||||
FieldMetadataDefaultValueRawJson,
|
||||
FieldMetadataDefaultValueRichTextV2,
|
||||
FieldMetadataDefaultValueString,
|
||||
FieldMetadataDefaultValueStringArray,
|
||||
FieldMetadataDefaultValueUuidFunction,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
export const defaultValueValidatorsMap = {
|
||||
[FieldMetadataType.UUID]: [
|
||||
FieldMetadataDefaultValueString,
|
||||
FieldMetadataDefaultValueUuidFunction,
|
||||
],
|
||||
[FieldMetadataType.TEXT]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.DATE_TIME]: [
|
||||
FieldMetadataDefaultValueDateTime,
|
||||
FieldMetadataDefaultValueNowFunction,
|
||||
],
|
||||
[FieldMetadataType.DATE]: [FieldMetadataDefaultValueDate],
|
||||
[FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean],
|
||||
[FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber],
|
||||
[FieldMetadataType.NUMERIC]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.CURRENCY]: [FieldMetadataDefaultValueCurrency],
|
||||
[FieldMetadataType.FULL_NAME]: [FieldMetadataDefaultValueFullName],
|
||||
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
|
||||
[FieldMetadataType.ADDRESS]: [FieldMetadataDefaultValueAddress],
|
||||
[FieldMetadataType.RICH_TEXT_V2]: [FieldMetadataDefaultValueRichTextV2],
|
||||
[FieldMetadataType.RICH_TEXT]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
|
||||
[FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks],
|
||||
[FieldMetadataType.ACTOR]: [FieldMetadataDefaultActor],
|
||||
[FieldMetadataType.EMAILS]: [FieldMetadataDefaultValueEmails],
|
||||
[FieldMetadataType.PHONES]: [FieldMetadataDefaultValuePhones],
|
||||
};
|
||||
|
||||
type ValidationResult = {
|
||||
isValid: boolean;
|
||||
errors: ValidationError[];
|
||||
};
|
||||
|
||||
export const validateDefaultValueForType = (
|
||||
type: FieldMetadataType,
|
||||
defaultValue: FieldMetadataDefaultValue,
|
||||
): ValidationResult => {
|
||||
if (defaultValue === null) {
|
||||
return {
|
||||
isValid: true,
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const validators = defaultValueValidatorsMap[type] as any[];
|
||||
|
||||
if (!validators) {
|
||||
return {
|
||||
isValid: false,
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
|
||||
const validationResults = validators.map((validator) => {
|
||||
const computedDefaultValue = isCompositeFieldMetadataType(type)
|
||||
? defaultValue
|
||||
: { value: defaultValue };
|
||||
|
||||
const defaultValueInstance = plainToInstance<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any,
|
||||
FieldMetadataClassValidation
|
||||
>(validator, computedDefaultValue as FieldMetadataClassValidation);
|
||||
|
||||
const errors = validateSync(defaultValueInstance, {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
||||
const isValid = errors.length === 0;
|
||||
|
||||
return {
|
||||
isValid,
|
||||
errors,
|
||||
};
|
||||
});
|
||||
|
||||
const isValid = validationResults.some((result) => result.isValid);
|
||||
|
||||
return {
|
||||
isValid,
|
||||
errors: validationResults.flatMap((result) => result.errors),
|
||||
};
|
||||
};
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import { type Relation } from 'typeorm';
|
||||
|
||||
import { type AllNonWorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/all-non-workspace-related-entity.type';
|
||||
import { type WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
|
|
@ -10,7 +8,7 @@ export type ExtractEntityManyToOneEntityRelationProperties<
|
|||
{
|
||||
[P in keyof T]: [NonNullable<T[P]>] extends [never]
|
||||
? never
|
||||
: NonNullable<T[P]> extends Relation<TTarget>
|
||||
: NonNullable<T[P]> extends TTarget
|
||||
? P
|
||||
: never;
|
||||
}[keyof T]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { type Relation } from 'typeorm';
|
||||
|
||||
import { type AllNonWorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/all-non-workspace-related-entity.type';
|
||||
import { type WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
|
|
@ -9,7 +7,7 @@ export type ExtractEntityOneToManyEntityRelationProperties<
|
|||
> = NonNullable<
|
||||
{
|
||||
[P in keyof T]: NonNullable<T[P]> extends Array<infer U>
|
||||
? U extends Relation<TTarget>
|
||||
? U extends TTarget
|
||||
? P
|
||||
: never
|
||||
: never;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import {
|
||||
type FieldMetadataRelationSettings,
|
||||
type FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
RelationType,
|
||||
|
|
@ -285,9 +285,9 @@ describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () =>
|
|||
input.targetFlatObjectMetadata.id,
|
||||
);
|
||||
const sourceSettings =
|
||||
sourceFieldMetadata.settings as FieldMetadataRelationSettings;
|
||||
sourceFieldMetadata.settings as FieldMetadataSettingsMapping['RELATION'];
|
||||
const targetSettings =
|
||||
targetFieldMetadata.settings as FieldMetadataRelationSettings;
|
||||
targetFieldMetadata.settings as FieldMetadataSettingsMapping['RELATION'];
|
||||
|
||||
expect(sourceSettings.relationType).toBe(expectedSourceRelationType);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
|
@ -63,7 +64,7 @@ export class ObjectMetadataEntity
|
|||
icon: string | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
standardOverrides: ObjectStandardOverridesDTO | null;
|
||||
standardOverrides: JsonbProperty<ObjectStandardOverridesDTO> | null;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
|
|
@ -93,7 +94,7 @@ export class ObjectMetadataEntity
|
|||
isSearchable: boolean;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
duplicateCriteria: WorkspaceEntityDuplicateCriteria[] | null;
|
||||
duplicateCriteria: JsonbProperty<WorkspaceEntityDuplicateCriteria[]> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
shortcut: string | null;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { CalendarStartDay } from 'twenty-shared/constants';
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
|
@ -37,7 +38,7 @@ export class AggregateChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
aggregateFieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { CalendarStartDay } from 'twenty-shared/constants';
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
|
@ -41,7 +42,7 @@ export class BarChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
aggregateFieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
|
|
@ -51,7 +52,7 @@ export class BarChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
primaryAxisGroupByFieldMetadataId: string;
|
||||
primaryAxisGroupByFieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
|
|
@ -80,7 +81,7 @@ export class BarChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
secondaryAxisGroupByFieldMetadataId?: string;
|
||||
secondaryAxisGroupByFieldMetadataId?: SerializedRelation;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { CalendarStartDay } from 'twenty-shared/constants';
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
|
@ -34,7 +35,7 @@ export class GaugeChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
aggregateFieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { CalendarStartDay } from 'twenty-shared/constants';
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
|
@ -39,7 +40,7 @@ export class LineChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
aggregateFieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
|
|
@ -49,7 +50,7 @@ export class LineChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
primaryAxisGroupByFieldMetadataId: string;
|
||||
primaryAxisGroupByFieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
|
|
@ -81,7 +82,7 @@ export class LineChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
secondaryAxisGroupByFieldMetadataId?: string;
|
||||
secondaryAxisGroupByFieldMetadataId?: SerializedRelation;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { CalendarStartDay } from 'twenty-shared/constants';
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
|
@ -37,7 +38,7 @@ export class PieChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
aggregateFieldMetadataId: string;
|
||||
aggregateFieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => AggregateOperations)
|
||||
@IsEnum(AggregateOperations)
|
||||
|
|
@ -47,7 +48,7 @@ export class PieChartConfigurationDTO
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
groupByFieldMetadataId: string;
|
||||
groupByFieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ export class RatioAggregateConfigDTO {
|
|||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
fieldMetadataId: string;
|
||||
fieldMetadataId: SerializedRelation;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums
|
|||
import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type';
|
||||
import { PageLayoutWidgetConfigurationTypeSettings } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configuration.type';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
@Entity({ name: 'pageLayoutWidget', schema: 'core' })
|
||||
@ObjectType('PageLayoutWidget')
|
||||
|
|
@ -70,10 +71,12 @@ export class PageLayoutWidgetEntity<
|
|||
objectMetadata: Relation<ObjectMetadataEntity> | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
gridPosition: GridPosition;
|
||||
gridPosition: JsonbProperty<GridPosition>;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
configuration: PageLayoutWidgetConfigurationTypeSettings<TWidgetConfigurationType>;
|
||||
configuration: JsonbProperty<
|
||||
PageLayoutWidgetConfigurationTypeSettings<TWidgetConfigurationType>
|
||||
>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ export class RouteTriggerEntity
|
|||
httpMethod: HTTPMethod;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb', default: [] })
|
||||
forwardedRequestHeaders: string[];
|
||||
forwardedRequestHeaders: JsonbProperty<string[]>;
|
||||
|
||||
@ManyToOne(
|
||||
() => ServerlessFunctionEntity,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
|||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
@Entity({ name: 'rowLevelPermissionPredicate', schema: 'core' })
|
||||
@Index('IDX_RLPP_WORKSPACE_ID_ROLE_ID_OBJECT_METADATA_ID', [
|
||||
|
|
@ -71,7 +72,7 @@ export class RowLevelPermissionPredicateEntity
|
|||
operand: RowLevelPermissionPredicateOperand;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
value: RowLevelPermissionPredicateValue | null;
|
||||
value: JsonbProperty<RowLevelPermissionPredicateValue> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'text', default: null })
|
||||
subFieldName: string | null;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
|
||||
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
|
||||
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
|
||||
|
|
@ -59,7 +60,7 @@ export class ServerlessFunctionEntity
|
|||
latestVersion: string | null;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb', default: [] })
|
||||
publishedVersions: string[];
|
||||
publishedVersions: JsonbProperty<string[]>;
|
||||
|
||||
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE22 })
|
||||
runtime: ServerlessFunctionRuntime;
|
||||
|
|
@ -72,7 +73,7 @@ export class ServerlessFunctionEntity
|
|||
checksum: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
toolInputSchema: object | null;
|
||||
toolInputSchema: JsonbProperty<object> | null;
|
||||
|
||||
@Column({ nullable: false, default: false })
|
||||
isTool: boolean;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { type ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type';
|
||||
|
|
@ -47,7 +48,7 @@ export class ViewFilterEntity
|
|||
operand: ViewFilterOperand;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb' })
|
||||
value: ViewFilterValue;
|
||||
value: JsonbProperty<ViewFilterValue>;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
viewFilterGroupId: string | null;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,4 @@ export type ViewFilterValue =
|
|||
| boolean
|
||||
| number
|
||||
| RelationFilterValue
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| undefined;
|
||||
| Record<string, unknown>;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import {
|
||||
type FieldMetadataType,
|
||||
type FieldMetadataSettings,
|
||||
type FieldMetadataOptions,
|
||||
type FieldMetadataSettings,
|
||||
type FieldMetadataType,
|
||||
type FieldMetadataDefaultValue,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { type Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
type FieldMetadataRelationSettings,
|
||||
type FieldMetadataSettingsMapping,
|
||||
RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const formatColumnNameForRelationField = (
|
||||
fieldName: string,
|
||||
fieldMetadataSettings: FieldMetadataRelationSettings,
|
||||
fieldMetadataSettings: FieldMetadataSettingsMapping['RELATION'],
|
||||
): string => {
|
||||
if (fieldMetadataSettings.relationType === RelationType.ONE_TO_MANY) {
|
||||
throw new Error('No column exists for one to many relation fields');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import { type Equal, type Expect } from 'twenty-shared/testing';
|
||||
|
||||
import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
type EmptyObject = {};
|
||||
|
||||
type TestedRecord = {
|
||||
// Non-JsonbProperty fields
|
||||
plainString: string;
|
||||
plainNumber: number;
|
||||
plainObject: EmptyObject;
|
||||
plainObjectNullable: EmptyObject | null;
|
||||
plainArray: string[];
|
||||
plainUnknown: unknown;
|
||||
jsonbString: JsonbProperty<string>;
|
||||
jsonbPlainUnknown: JsonbProperty<unknown>;
|
||||
jsonbNumber: JsonbProperty<number>;
|
||||
jsonbull: JsonbProperty<null>;
|
||||
|
||||
// JsonbProperty fields - should be extracted
|
||||
jsonbPlainObject: JsonbProperty<EmptyObject>;
|
||||
jsonbPlainArray: JsonbProperty<string[]>;
|
||||
jsonbPlainObjectNullable: JsonbProperty<EmptyObject | null>;
|
||||
jsonbEmpty: JsonbProperty<EmptyObject>;
|
||||
jsonbArray: JsonbProperty<string[]>;
|
||||
jsonbNested: JsonbProperty<{ nested: { deep: number } }>;
|
||||
jsonbNullable: JsonbProperty<EmptyObject> | null;
|
||||
jsonbUndefinable: JsonbProperty<EmptyObject> | undefined;
|
||||
jsonbOptional?: JsonbProperty<EmptyObject>;
|
||||
jsonbInnerNullable: JsonbProperty<EmptyObject | null>;
|
||||
jsonbInnerUndefinable: JsonbProperty<EmptyObject | undefined>;
|
||||
jsonbUnionWithPrimitive: JsonbProperty<EmptyObject> | string | null;
|
||||
jsonbInnerNullableWithProperties: JsonbProperty<null | { value: string }>;
|
||||
wrongUsageButPassing:
|
||||
| JsonbProperty<null | { value: string }>
|
||||
| string
|
||||
| { foo: string };
|
||||
};
|
||||
|
||||
type TestResult = ExtractJsonbProperties<TestedRecord>;
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type Assertions = [
|
||||
Expect<
|
||||
Equal<
|
||||
TestResult,
|
||||
| 'jsonbPlainObject'
|
||||
| 'jsonbPlainArray'
|
||||
| 'jsonbPlainObjectNullable'
|
||||
| 'jsonbEmpty'
|
||||
| 'jsonbArray'
|
||||
| 'jsonbNested'
|
||||
| 'jsonbNullable'
|
||||
| 'jsonbUndefinable'
|
||||
| 'jsonbOptional'
|
||||
| 'jsonbInnerNullable'
|
||||
| 'jsonbInnerUndefinable'
|
||||
| 'jsonbInnerNullableWithProperties'
|
||||
| 'jsonbUnionWithPrimitive'
|
||||
| 'wrongUsageButPassing'
|
||||
>
|
||||
>,
|
||||
|
||||
// Empty object returns never
|
||||
Expect<Equal<ExtractJsonbProperties<EmptyObject>, never>>,
|
||||
|
||||
// Object with no JsonbProperty fields returns never
|
||||
Expect<Equal<ExtractJsonbProperties<{ a: string; b: number }>, never>>,
|
||||
];
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import { type Equal, type Expect } from 'twenty-shared/testing';
|
||||
import { type SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { type FormatJsonbSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type';
|
||||
import {
|
||||
type JSONB_PROPERTY_BRAND,
|
||||
type JsonbProperty,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
type BrandedObjectWithRelation = JsonbProperty<{
|
||||
name: string;
|
||||
targetFieldMetadataId: SerializedRelation;
|
||||
}>;
|
||||
|
||||
type BrandedObjectWithoutRelation = JsonbProperty<{
|
||||
name: string;
|
||||
count: number;
|
||||
}>;
|
||||
|
||||
type UnbrandedObject = {
|
||||
name: string;
|
||||
targetFieldMetadataId: SerializedRelation;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type BrandedObjectAssertions = [
|
||||
// Branded object with SerializedRelation: Id suffix renamed to UniversalIdentifier
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
// Branded object without SerializedRelation: no renaming, just removes brand
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithoutRelation>,
|
||||
{
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type UnbrandedObjectAssertions = [
|
||||
// Unbranded objects pass through unchanged
|
||||
Expect<
|
||||
Equal<FormatJsonbSerializedRelation<UnbrandedObject>, UnbrandedObject>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type PrimitiveAssertions = [
|
||||
// Primitives pass through unchanged
|
||||
Expect<Equal<FormatJsonbSerializedRelation<string>, string>>,
|
||||
Expect<Equal<FormatJsonbSerializedRelation<number>, number>>,
|
||||
Expect<Equal<FormatJsonbSerializedRelation<null>, null>>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type ArrayAssertions = [
|
||||
// Array of branded objects: transforms each element
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation[]>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}[]
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of unbranded objects: passes through unchanged
|
||||
Expect<
|
||||
Equal<FormatJsonbSerializedRelation<UnbrandedObject[]>, UnbrandedObject[]>
|
||||
>,
|
||||
|
||||
// Array of primitives: passes through unchanged
|
||||
Expect<Equal<FormatJsonbSerializedRelation<string[]>, string[]>>,
|
||||
|
||||
// Nested array of branded objects: transforms innermost elements
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation[][]>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}[][]
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of unbranded and branded objects union: transforms branded element
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<
|
||||
(BrandedObjectWithRelation | UnbrandedObject)[]
|
||||
>,
|
||||
(
|
||||
| {
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}
|
||||
| UnbrandedObject
|
||||
)[]
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type UnionAssertions = [
|
||||
// Union with null: transforms branded object, keeps null
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation | null>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
} | null
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of union: transforms elements appropriately
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<(BrandedObjectWithRelation | null)[]>,
|
||||
({
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
} | null)[]
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
type MultipleRelationsObject = JsonbProperty<{
|
||||
name: string;
|
||||
sourceFieldId: SerializedRelation;
|
||||
targetFieldId: SerializedRelation;
|
||||
regularId: string;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type MultipleRelationsAssertions = [
|
||||
// Multiple SerializedRelation properties: all get renamed
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<MultipleRelationsObject>,
|
||||
{
|
||||
name: string;
|
||||
sourceFieldUniversalIdentifier: SerializedRelation;
|
||||
targetFieldUniversalIdentifier: SerializedRelation;
|
||||
regularId: string;
|
||||
}
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// Verify brand is removed
|
||||
type BrandRemovedCheck =
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation>;
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type BrandRemovedAssertion = Expect<
|
||||
Equal<
|
||||
typeof JSONB_PROPERTY_BRAND extends keyof BrandRemovedCheck ? true : false,
|
||||
false
|
||||
>
|
||||
>;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { type Equal, type Expect } from 'twenty-shared/testing';
|
||||
|
||||
import {
|
||||
type JSONB_PROPERTY_BRAND,
|
||||
type JsonbProperty,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
type EmptyObject = {};
|
||||
|
||||
type SimpleObject = { value: string };
|
||||
|
||||
type NestedObject = { nested: { deep: number } };
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type PrimitiveAssertions = [
|
||||
// Primitives pass through unchanged (not objects)
|
||||
Expect<Equal<JsonbProperty<string>, string>>,
|
||||
Expect<Equal<JsonbProperty<number>, number>>,
|
||||
Expect<Equal<JsonbProperty<boolean>, boolean>>,
|
||||
Expect<Equal<JsonbProperty<null>, null>>,
|
||||
Expect<Equal<JsonbProperty<undefined>, undefined>>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type ObjectAssertions = [
|
||||
// Objects get branded
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<SimpleObject>,
|
||||
SimpleObject & { [JSONB_PROPERTY_BRAND]?: never }
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<NestedObject>,
|
||||
NestedObject & { [JSONB_PROPERTY_BRAND]?: never }
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<EmptyObject>,
|
||||
EmptyObject & { [JSONB_PROPERTY_BRAND]?: never }
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type ArrayAssertions = [
|
||||
// Arrays are objects, so they get branded on the array itself
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<string[]>,
|
||||
string[] & { [JSONB_PROPERTY_BRAND]?: never }
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<number[]>,
|
||||
number[] & { [JSONB_PROPERTY_BRAND]?: never }
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<SimpleObject[]>,
|
||||
SimpleObject[] & { [JSONB_PROPERTY_BRAND]?: never }
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type UnionAssertions = [
|
||||
// Union of object and null: object gets branded, null passes through
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<SimpleObject | null>,
|
||||
(SimpleObject & { [JSONB_PROPERTY_BRAND]?: never }) | null
|
||||
>
|
||||
>,
|
||||
|
||||
// Union of objects: both get branded (distributive conditional)
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<SimpleObject | NestedObject>,
|
||||
| (SimpleObject & { [JSONB_PROPERTY_BRAND]?: never })
|
||||
| (NestedObject & { [JSONB_PROPERTY_BRAND]?: never })
|
||||
>
|
||||
>,
|
||||
|
||||
// Array in union: array gets branded
|
||||
Expect<
|
||||
Equal<
|
||||
JsonbProperty<SimpleObject[] | null>,
|
||||
(SimpleObject[] & { [JSONB_PROPERTY_BRAND]?: never }) | null
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
|
@ -1,7 +1,18 @@
|
|||
import { type Expect, type HasAllProperties } from 'twenty-shared/testing';
|
||||
import {
|
||||
type Equal,
|
||||
type Expect,
|
||||
type HasAllProperties,
|
||||
} from 'twenty-shared/testing';
|
||||
import {
|
||||
type FieldMetadataDefaultOption,
|
||||
type FieldMetadataType,
|
||||
type FieldNumberVariant,
|
||||
type LinkMetadata,
|
||||
type NullablePartial,
|
||||
type NumberDataType,
|
||||
type RelationOnDeleteAction,
|
||||
type RelationType,
|
||||
type SerializedRelation,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
|
|
@ -79,3 +90,69 @@ type UniversalFlatTransformationAssertions = [
|
|||
>
|
||||
>,
|
||||
];
|
||||
|
||||
type NarrowedTestCase =
|
||||
UniversalFlatFieldMetadata<FieldMetadataType.RELATION>['settings'];
|
||||
|
||||
type NarrowedExpectedResult = {
|
||||
relationType: RelationType;
|
||||
onDelete?: RelationOnDeleteAction | undefined;
|
||||
joinColumnName?: string | null | undefined;
|
||||
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
|
||||
};
|
||||
|
||||
type SettingsTestCase = UniversalFlatFieldMetadata<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.NUMBER | FieldMetadataType.TEXT
|
||||
>['settings'];
|
||||
|
||||
type SettingsExpectedResult =
|
||||
| {
|
||||
relationType: RelationType;
|
||||
onDelete?: RelationOnDeleteAction | undefined;
|
||||
joinColumnName?: string | null | undefined;
|
||||
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
|
||||
}
|
||||
| {
|
||||
dataType?: NumberDataType | undefined;
|
||||
decimals?: number | undefined;
|
||||
type?: FieldNumberVariant | undefined;
|
||||
}
|
||||
| {
|
||||
displayedMaxRows?: number | undefined;
|
||||
}
|
||||
| null;
|
||||
|
||||
type DefaultValueTestCase = UniversalFlatFieldMetadata<
|
||||
| FieldMetadataType.RELATION
|
||||
| FieldMetadataType.NUMBER
|
||||
| FieldMetadataType.TEXT
|
||||
| FieldMetadataType.LINKS
|
||||
| FieldMetadataType.CURRENCY
|
||||
>['defaultValue'];
|
||||
|
||||
type DefaultValueExpectedResult =
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
| {
|
||||
amountMicros: string | null;
|
||||
currencyCode: string | null;
|
||||
}
|
||||
| {
|
||||
primaryLinkLabel: string | null;
|
||||
primaryLinkUrl: string | null;
|
||||
secondaryLinks: LinkMetadata[] | null;
|
||||
};
|
||||
|
||||
type OptionsTestCase =
|
||||
UniversalFlatFieldMetadata<FieldMetadataType.RATING>['options'];
|
||||
|
||||
type OptionsExpectedResult = FieldMetadataDefaultOption[];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type Assertions = [
|
||||
Expect<Equal<SettingsTestCase, SettingsExpectedResult>>,
|
||||
Expect<Equal<NarrowedTestCase, NarrowedExpectedResult>>,
|
||||
Expect<Equal<DefaultValueTestCase, DefaultValueExpectedResult>>,
|
||||
Expect<Equal<OptionsTestCase, OptionsExpectedResult>>,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { type JSONB_PROPERTY_BRAND } from './jsonb-property.type';
|
||||
|
||||
export type HasJsonbPropertyBrand<T> =
|
||||
typeof JSONB_PROPERTY_BRAND extends keyof T ? true : false;
|
||||
|
||||
// Distributive check: returns `true` if any member of a union has the brand
|
||||
type HasJsonbBrandInUnion<T> = T extends unknown
|
||||
? HasJsonbPropertyBrand<T>
|
||||
: never;
|
||||
|
||||
export type ExtractJsonbProperties<T> = NonNullable<
|
||||
{
|
||||
[P in keyof T]-?: true extends HasJsonbBrandInUnion<NonNullable<T[P]>>
|
||||
? P
|
||||
: never;
|
||||
}[keyof T]
|
||||
>;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { type ExtractSerializedRelationProperties } from 'twenty-shared/types';
|
||||
|
||||
import { type HasJsonbPropertyBrand } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
|
||||
import { type JSONB_PROPERTY_BRAND } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
|
||||
|
||||
export type FormatJsonbSerializedRelation<T> = T extends unknown
|
||||
? T extends (infer U)[]
|
||||
? FormatJsonbSerializedRelation<U>[]
|
||||
: HasJsonbPropertyBrand<T> extends true
|
||||
? Omit<
|
||||
{
|
||||
[P in keyof T as P extends ExtractSerializedRelationProperties<T> &
|
||||
string
|
||||
? `${RemoveSuffix<P, 'Id'>}UniversalIdentifier`
|
||||
: P]: T[P];
|
||||
},
|
||||
typeof JSONB_PROPERTY_BRAND
|
||||
>
|
||||
: T
|
||||
: never;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export const JSONB_PROPERTY_BRAND = '__JsonbPropertyBrand__' as const;
|
||||
|
||||
export type JsonbProperty<T> = T extends unknown
|
||||
? T extends object
|
||||
? T & { [JSONB_PROPERTY_BRAND]?: never }
|
||||
: T
|
||||
: never;
|
||||
|
|
@ -6,6 +6,8 @@ import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-m
|
|||
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
|
||||
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
|
||||
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
|
||||
import { type FormatJsonbSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type';
|
||||
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
|
||||
|
||||
// TODO Handle universal settings
|
||||
|
|
@ -22,6 +24,7 @@ export type UniversalFlatEntityFrom<
|
|||
| ExtractEntityRelatedEntityProperties<TEntity>
|
||||
| Extract<MetadataManyToOneJoinColumn<TMetadataName>, keyof TEntity>
|
||||
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
|
||||
| ExtractJsonbProperties<TEntity>
|
||||
> &
|
||||
CastRecordTypeOrmDatePropertiesToString<TEntity> & {
|
||||
[P in ExtractEntityOneToManyEntityRelationProperties<
|
||||
|
|
@ -34,4 +37,8 @@ export type UniversalFlatEntityFrom<
|
|||
string as `${RemoveSuffix<P, 'Id'>}UniversalIdentifier`]: TEntity[P];
|
||||
} & {
|
||||
applicationUniversalIdentifier: string;
|
||||
} & {
|
||||
[P in ExtractJsonbProperties<TEntity>]: FormatJsonbSerializedRelation<
|
||||
TEntity[P]
|
||||
>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { type ColumnType } from 'typeorm';
|
||||
|
||||
import { type FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
FieldMetadataException,
|
||||
|
|
@ -11,7 +10,7 @@ import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field
|
|||
import { removeSqlDDLInjection } from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
|
||||
|
||||
type SerializeDefaultValueArgs = {
|
||||
defaultValue?: FieldMetadataDefaultSerializableValue;
|
||||
defaultValue?: FieldMetadataDefaultValueForAnyType;
|
||||
columnType?: ColumnType;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import {
|
|||
type FieldMetadataTypesToTestForFilterInputValidation,
|
||||
} from 'test/integration/graphql/suites/inputs-validation/types/field-metadata-type-to-test';
|
||||
import {
|
||||
type FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
RelationType,
|
||||
type FieldMetadataFilesSettings,
|
||||
type FieldMetadataMultiItemSettings,
|
||||
type RelationCreationPayload,
|
||||
} from 'twenty-shared/types';
|
||||
|
|
@ -20,7 +20,9 @@ type FieldMetadataCreationInput = {
|
|||
options?: FieldMetadataComplexOption[];
|
||||
relationCreationPayload?: RelationCreationPayload;
|
||||
morphRelationsCreationPayload?: RelationCreationPayload[];
|
||||
settings?: FieldMetadataMultiItemSettings | FieldMetadataFilesSettings;
|
||||
settings?:
|
||||
| FieldMetadataMultiItemSettings
|
||||
| FieldMetadataSettingsMapping['FILES'];
|
||||
};
|
||||
|
||||
export const getFieldMetadataCreationInputs = (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
import { type UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
|
||||
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view-filter-group/enums/view-filter-group-logical-operator';
|
||||
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { type ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
|
||||
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
|
|
@ -25,33 +19,6 @@ export const createViewData = (overrides: Partial<ViewEntity> = {}) => ({
|
|||
...overrides,
|
||||
});
|
||||
|
||||
export const updateViewData = (overrides: Partial<ViewEntity> = {}) => ({
|
||||
name: 'Updated View',
|
||||
type: ViewType.KANBAN,
|
||||
isCompact: true,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const createViewFieldData = (
|
||||
viewId: string,
|
||||
overrides: Partial<ViewFieldEntity> = {},
|
||||
) => ({
|
||||
viewId,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const updateViewFieldData = (
|
||||
overrides: Partial<UpdateViewFieldInput['update']> = {},
|
||||
) => ({
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 300,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const createViewSortData = (
|
||||
viewId: string,
|
||||
overrides: Partial<ViewSortEntity> = {},
|
||||
|
|
@ -68,44 +35,6 @@ export const updateViewSortData = (
|
|||
...overrides,
|
||||
});
|
||||
|
||||
export const createViewFilterData = (
|
||||
viewId: string,
|
||||
overrides: Partial<ViewFilterEntity> = {},
|
||||
) => ({
|
||||
viewId,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: 'test-value',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const updateViewFilterData = (
|
||||
overrides: Partial<ViewFilterEntity> = {},
|
||||
) => ({
|
||||
operand: ViewFilterOperand.IS_NOT,
|
||||
value: 'updated-value',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const createViewGroupData = (
|
||||
viewId: string,
|
||||
overrides: Partial<ViewGroupEntity> = {},
|
||||
) => ({
|
||||
viewId,
|
||||
fieldValue: 'test-group-value',
|
||||
isVisible: true,
|
||||
position: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const updateViewGroupData = (
|
||||
overrides: Partial<ViewGroupEntity> = {},
|
||||
) => ({
|
||||
fieldValue: 'updated-group-value',
|
||||
isVisible: false,
|
||||
position: 1,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const createViewFilterGroupData = (
|
||||
viewId: string,
|
||||
overrides: Partial<ViewFilterGroupEntity> = {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { type SERIALIZED_RELATION_BRAND } from './SerializedRelation.type';
|
||||
|
||||
type HasSerializedRelationPropertyBrand<T> =
|
||||
typeof SERIALIZED_RELATION_BRAND extends keyof T ? true : false;
|
||||
|
||||
export type ExtractSerializedRelationProperties<T> = T extends unknown
|
||||
? T extends object
|
||||
? {
|
||||
[P in keyof T]-?: [NonNullable<T[P]>] extends [never]
|
||||
? never
|
||||
: HasSerializedRelationPropertyBrand<NonNullable<T[P]>> extends true
|
||||
? P
|
||||
: never;
|
||||
}[keyof T]
|
||||
: never
|
||||
: never;
|
||||
|
|
@ -1,43 +1,7 @@
|
|||
import { type LinkMetadata } from '@/types/composite-types/links.composite-type';
|
||||
import { type FieldMetadataType } from '@/types/FieldMetadataType';
|
||||
import { type IsExactly } from '@/types/IsExactly';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsNumberString,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Matches,
|
||||
ValidateIf,
|
||||
type ValidationArguments,
|
||||
type ValidationOptions,
|
||||
registerDecorator,
|
||||
} from 'class-validator';
|
||||
|
||||
const IsQuotedString = (validationOptions?: ValidationOptions) => {
|
||||
return (object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
name: 'isQuotedString',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate: (value: any) => {
|
||||
return typeof value === 'string' && /^'{1}.*'{1}$/.test(value);
|
||||
},
|
||||
defaultMessage: (args: ValidationArguments) => {
|
||||
return `${args.property} must be a quoted string`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const fieldMetadataDefaultValueFunctionName = {
|
||||
UUID: 'uuid',
|
||||
NOW: 'now',
|
||||
|
|
@ -46,266 +10,105 @@ export const fieldMetadataDefaultValueFunctionName = {
|
|||
export type FieldMetadataDefaultValueFunctionNames =
|
||||
(typeof fieldMetadataDefaultValueFunctionName)[keyof typeof fieldMetadataDefaultValueFunctionName];
|
||||
|
||||
export class FieldMetadataDefaultValueString {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
value!: string | null;
|
||||
}
|
||||
export type FieldMetadataDefaultValueUuidFunction =
|
||||
typeof fieldMetadataDefaultValueFunctionName.UUID;
|
||||
export type FieldMetadataDefaultValueNowFunction =
|
||||
typeof fieldMetadataDefaultValueFunctionName.NOW;
|
||||
|
||||
export class FieldMetadataDefaultValueRawJson {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsObject() // TODO: Should this also allow arrays?
|
||||
value!: object | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueRichTextV2 {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
blocknote!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
markdown!: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueRichText {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
value!: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueNumber {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumber()
|
||||
value!: number | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueBoolean {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsBoolean()
|
||||
value!: boolean | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueStringArray {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsArray()
|
||||
@IsQuotedString({ each: true })
|
||||
value!: string[] | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueDateTime {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsDate()
|
||||
value!: Date | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueDate {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsDate()
|
||||
value!: Date | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueCurrency {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumberString()
|
||||
amountMicros!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
currencyCode!: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueFullName {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
firstName!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
lastName!: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueUuidFunction {
|
||||
@Matches(fieldMetadataDefaultValueFunctionName.UUID)
|
||||
@IsNotEmpty()
|
||||
value!: typeof fieldMetadataDefaultValueFunctionName.UUID;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueNowFunction {
|
||||
@Matches(fieldMetadataDefaultValueFunctionName.NOW)
|
||||
@IsNotEmpty()
|
||||
value!: typeof fieldMetadataDefaultValueFunctionName.NOW;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueAddress {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressStreet1!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressStreet2!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressCity!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressPostcode!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressState!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
addressCountry!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumber()
|
||||
addressLat!: number | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsNumber()
|
||||
addressLng!: number | null;
|
||||
}
|
||||
|
||||
class LinkMetadata {
|
||||
@IsString()
|
||||
label!: string;
|
||||
|
||||
@IsString()
|
||||
url!: string;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueLinks {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryLinkLabel!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryLinkUrl!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsArray()
|
||||
secondaryLinks!: LinkMetadata[] | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultActor {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
source!: string;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
workspaceMemberId?: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
name!: string;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueEmails {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryEmail!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsObject()
|
||||
additionalEmails!: object | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValuePhones {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryPhoneNumber!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryPhoneCountryCode!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsQuotedString()
|
||||
primaryPhoneCallingCode!: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsObject()
|
||||
additionalPhones!: object | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultArray {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsArray()
|
||||
value!: string[] | null;
|
||||
}
|
||||
|
||||
type ExtractValueType<T> = T extends { value: infer V } ? V : T;
|
||||
|
||||
type UnionOfValues<T> = T[keyof T];
|
||||
|
||||
type FieldMetadataDefaultValueMapping = {
|
||||
[FieldMetadataType.UUID]:
|
||||
| FieldMetadataDefaultValueString
|
||||
| FieldMetadataDefaultValueUuidFunction;
|
||||
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.PHONES]: FieldMetadataDefaultValuePhones;
|
||||
[FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails;
|
||||
[FieldMetadataType.DATE_TIME]:
|
||||
| FieldMetadataDefaultValueDateTime
|
||||
| FieldMetadataDefaultValueNowFunction;
|
||||
[FieldMetadataType.DATE]:
|
||||
| FieldMetadataDefaultValueDateTime
|
||||
| FieldMetadataDefaultValueNowFunction;
|
||||
[FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean;
|
||||
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
|
||||
[FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber;
|
||||
[FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.LINKS]: FieldMetadataDefaultValueLinks;
|
||||
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency;
|
||||
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName;
|
||||
[FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress;
|
||||
[FieldMetadataType.RATING]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.SELECT]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray;
|
||||
[FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
|
||||
[FieldMetadataType.RICH_TEXT]: FieldMetadataDefaultValueRichText;
|
||||
[FieldMetadataType.ACTOR]: FieldMetadataDefaultActor;
|
||||
[FieldMetadataType.ARRAY]: FieldMetadataDefaultArray;
|
||||
export type FieldMetadataDefaultValueRichTextV2 = {
|
||||
blocknote: string | null;
|
||||
markdown: string | null;
|
||||
};
|
||||
|
||||
export type FieldMetadataClassValidation =
|
||||
UnionOfValues<FieldMetadataDefaultValueMapping>;
|
||||
export type FieldMetadataDefaultValueCurrency = {
|
||||
amountMicros: string | null;
|
||||
currencyCode: string | null;
|
||||
};
|
||||
|
||||
export type FieldMetadataFunctionDefaultValue = ExtractValueType<
|
||||
FieldMetadataDefaultValueUuidFunction | FieldMetadataDefaultValueNowFunction
|
||||
>;
|
||||
export type FieldMetadataDefaultValueFullName = {
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultValueForType<
|
||||
T extends keyof FieldMetadataDefaultValueMapping,
|
||||
> = ExtractValueType<FieldMetadataDefaultValueMapping[T]> | null;
|
||||
export type FieldMetadataDefaultValueAddress = {
|
||||
addressStreet1: string | null;
|
||||
addressStreet2: string | null;
|
||||
addressCity: string | null;
|
||||
addressPostcode: string | null;
|
||||
addressState: string | null;
|
||||
addressCountry: string | null;
|
||||
addressLat: number | null;
|
||||
addressLng: number | null;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultValueForAnyType = ExtractValueType<
|
||||
UnionOfValues<FieldMetadataDefaultValueMapping>
|
||||
> | null;
|
||||
export type FieldMetadataDefaultValueLinks = {
|
||||
primaryLinkLabel: string | null;
|
||||
primaryLinkUrl: string | null;
|
||||
secondaryLinks: LinkMetadata[] | null;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultActor = {
|
||||
source: string;
|
||||
workspaceMemberId?: string | null;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultValueEmails = {
|
||||
primaryEmail: string | null;
|
||||
additionalEmails: object | null;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultValuePhones = {
|
||||
primaryPhoneNumber: string | null;
|
||||
primaryPhoneCountryCode: string | null;
|
||||
primaryPhoneCallingCode: string | null;
|
||||
additionalPhones: object | null;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultValueMapping = {
|
||||
[FieldMetadataType.UUID]:
|
||||
| string
|
||||
| FieldMetadataDefaultValueUuidFunction
|
||||
| null;
|
||||
[FieldMetadataType.TEXT]: string | null;
|
||||
[FieldMetadataType.PHONES]: FieldMetadataDefaultValuePhones | null;
|
||||
[FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails | null;
|
||||
[FieldMetadataType.DATE_TIME]:
|
||||
| Date
|
||||
| FieldMetadataDefaultValueNowFunction
|
||||
| null;
|
||||
[FieldMetadataType.DATE]: Date | FieldMetadataDefaultValueNowFunction | null;
|
||||
[FieldMetadataType.BOOLEAN]: boolean | null;
|
||||
[FieldMetadataType.NUMBER]: number | null;
|
||||
[FieldMetadataType.POSITION]: number | null;
|
||||
[FieldMetadataType.NUMERIC]: string | null;
|
||||
[FieldMetadataType.LINKS]: FieldMetadataDefaultValueLinks | null;
|
||||
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency | null;
|
||||
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName | null;
|
||||
[FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress | null;
|
||||
[FieldMetadataType.RATING]: string | null;
|
||||
[FieldMetadataType.SELECT]: string | null;
|
||||
[FieldMetadataType.MULTI_SELECT]: string[] | null;
|
||||
[FieldMetadataType.RAW_JSON]: object | null;
|
||||
[FieldMetadataType.RICH_TEXT]: string | null;
|
||||
[FieldMetadataType.RICH_TEXT_V2]: FieldMetadataDefaultValueRichTextV2 | null;
|
||||
[FieldMetadataType.ACTOR]: FieldMetadataDefaultActor | null;
|
||||
[FieldMetadataType.ARRAY]: string[] | null;
|
||||
};
|
||||
|
||||
export type FieldMetadataFunctionDefaultValue =
|
||||
| FieldMetadataDefaultValueUuidFunction
|
||||
| FieldMetadataDefaultValueNowFunction;
|
||||
|
||||
export type FieldMetadataDefaultValueForAnyType =
|
||||
| null
|
||||
| FieldMetadataDefaultValueMapping[keyof FieldMetadataDefaultValueMapping];
|
||||
|
||||
export type FieldMetadataDefaultValue<
|
||||
T extends FieldMetadataType = FieldMetadataType,
|
||||
> =
|
||||
IsExactly<T, FieldMetadataType> extends true
|
||||
? FieldMetadataDefaultValueForAnyType | null // Could be improved to be | unknown
|
||||
? FieldMetadataDefaultValueForAnyType
|
||||
: T extends keyof FieldMetadataDefaultValueMapping
|
||||
? FieldMetadataDefaultValueForType<T>
|
||||
? FieldMetadataDefaultValueMapping[T]
|
||||
: never | null;
|
||||
|
||||
type FieldMetadataDefaultValueExtractedTypes = {
|
||||
[K in keyof FieldMetadataDefaultValueMapping]: ExtractValueType<
|
||||
FieldMetadataDefaultValueMapping[K]
|
||||
>;
|
||||
};
|
||||
|
||||
export type FieldMetadataDefaultSerializableValue =
|
||||
| FieldMetadataDefaultValueExtractedTypes[keyof FieldMetadataDefaultValueExtractedTypes]
|
||||
| null;
|
||||
|
|
|
|||
|
|
@ -30,11 +30,15 @@ type FieldMetadataOptionsMapping = {
|
|||
[FieldMetadataType.MULTI_SELECT]: FieldMetadataComplexOption[];
|
||||
};
|
||||
|
||||
export type FieldMetadataOptionForAnyType =
|
||||
| null
|
||||
| FieldMetadataOptionsMapping[keyof FieldMetadataOptionsMapping];
|
||||
|
||||
export type FieldMetadataOptions<
|
||||
T extends FieldMetadataType = FieldMetadataType,
|
||||
> =
|
||||
IsExactly<T, FieldMetadataType> extends true
|
||||
? null | (FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]) // Could be improved to be | unknown
|
||||
? FieldMetadataOptionForAnyType
|
||||
: T extends keyof FieldMetadataOptionsMapping
|
||||
? FieldMetadataOptionsMapping[T]
|
||||
: never | null;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { type FieldMetadataType } from '@/types/FieldMetadataType';
|
|||
import { type IsExactly } from '@/types/IsExactly';
|
||||
import { type RelationOnDeleteAction } from '@/types/RelationOnDeleteAction.type';
|
||||
import { type RelationType } from '@/types/RelationType';
|
||||
import { type SerializedRelation } from '@/types/SerializedRelation.type';
|
||||
|
||||
export enum NumberDataType {
|
||||
FLOAT = 'float',
|
||||
|
|
@ -19,46 +20,47 @@ export enum DateDisplayFormat {
|
|||
|
||||
export type FieldNumberVariant = 'number' | 'percentage';
|
||||
|
||||
export type FieldMetadataNumberSettings = {
|
||||
type FieldMetadataNumberSettings = {
|
||||
dataType?: NumberDataType;
|
||||
decimals?: number;
|
||||
type?: FieldNumberVariant;
|
||||
};
|
||||
|
||||
export type FieldMetadataTextSettings = {
|
||||
type FieldMetadataTextSettings = {
|
||||
displayedMaxRows?: number;
|
||||
};
|
||||
|
||||
export type FieldMetadataDateSettings = {
|
||||
type FieldMetadataDateSettings = {
|
||||
displayFormat?: DateDisplayFormat;
|
||||
};
|
||||
|
||||
export type FieldMetadataDateTimeSettings = {
|
||||
type FieldMetadataDateTimeSettings = {
|
||||
displayFormat?: DateDisplayFormat;
|
||||
};
|
||||
|
||||
export type FieldMetadataRelationSettings = {
|
||||
type FieldMetadataRelationSettings = {
|
||||
relationType: RelationType;
|
||||
onDelete?: RelationOnDeleteAction;
|
||||
joinColumnName?: string | null;
|
||||
// Points to the target field on the junction object
|
||||
// For MORPH_RELATION fields, morphRelations already contains all targets
|
||||
junctionTargetFieldId?: string;
|
||||
junctionTargetFieldId?: SerializedRelation;
|
||||
};
|
||||
export type FieldMetadataAddressSettings = {
|
||||
|
||||
type FieldMetadataAddressSettings = {
|
||||
subFields?: AllowedAddressSubField[];
|
||||
};
|
||||
|
||||
export type FieldMetadataFilesSettings = {
|
||||
type FieldMetadataFilesSettings = {
|
||||
maxNumberOfValues: number;
|
||||
};
|
||||
|
||||
export type FieldMetadataTsVectorSettings = {
|
||||
type FieldMetadataTsVectorSettings = {
|
||||
asExpression?: string;
|
||||
generatedType?: 'STORED' | 'VIRTUAL';
|
||||
};
|
||||
|
||||
type FieldMetadataSettingsMapping = {
|
||||
export type FieldMetadataSettingsMapping = {
|
||||
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings | null;
|
||||
[FieldMetadataType.DATE]: FieldMetadataDateSettings | null;
|
||||
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings | null;
|
||||
|
|
@ -81,7 +83,7 @@ export type FieldMetadataSettings<
|
|||
T extends FieldMetadataType = FieldMetadataType,
|
||||
> =
|
||||
IsExactly<T, FieldMetadataType> extends true
|
||||
? null | AllFieldMetadataSettings // Could be improved to be | unknown
|
||||
? null | AllFieldMetadataSettings
|
||||
: T extends keyof FieldMetadataSettingsMapping
|
||||
? FieldMetadataSettingsMapping[T]
|
||||
: never | null;
|
||||
|
|
|
|||
|
|
@ -12,5 +12,4 @@ export type RowLevelPermissionPredicateValue =
|
|||
| number
|
||||
| RelationPredicateValue
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| undefined;
|
||||
| null;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export const SERIALIZED_RELATION_BRAND = '__SerializedRelationBrand__' as const;
|
||||
|
||||
export type SerializedRelation = string & {
|
||||
[SERIALIZED_RELATION_BRAND]?: never;
|
||||
};
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { Equal, Expect } from '@/testing';
|
||||
import { type ExtractSerializedRelationProperties } from '@/types/ExtractSerializedRelationProperties.type';
|
||||
import { type SerializedRelation } from '@/types/SerializedRelation.type';
|
||||
|
||||
type TestedRecord = {
|
||||
// Non-SerializedRelation fields
|
||||
plainString: string;
|
||||
plainNumber: number;
|
||||
plainBoolean: boolean;
|
||||
plainObject: { id: string };
|
||||
plainArray: string[];
|
||||
plainUnknown: unknown;
|
||||
plainStringNullable: string | null;
|
||||
plainStringOptional?: string;
|
||||
|
||||
// SerializedRelation fields - should be extracted
|
||||
relation: SerializedRelation;
|
||||
relationNullable: SerializedRelation | null;
|
||||
relationUndefinable: SerializedRelation | undefined;
|
||||
relationOptional?: SerializedRelation;
|
||||
relationOptionalNullable?: SerializedRelation | null;
|
||||
};
|
||||
|
||||
type TestResult = ExtractSerializedRelationProperties<TestedRecord>;
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type Assertions = [
|
||||
Expect<
|
||||
Equal<
|
||||
TestResult,
|
||||
| 'relation'
|
||||
| 'relationNullable'
|
||||
| 'relationUndefinable'
|
||||
| 'relationOptional'
|
||||
| 'relationOptionalNullable'
|
||||
>
|
||||
>,
|
||||
|
||||
// Object with no SerializedRelation fields returns never
|
||||
Expect<
|
||||
Equal<ExtractSerializedRelationProperties<{ a: string; b: number }>, never>
|
||||
>,
|
||||
|
||||
// Primitives return never (tests T extends object branch)
|
||||
Expect<Equal<ExtractSerializedRelationProperties<string>, never>>,
|
||||
Expect<Equal<ExtractSerializedRelationProperties<number>, never>>,
|
||||
Expect<Equal<ExtractSerializedRelationProperties<null>, never>>,
|
||||
|
||||
// Empty object returns never
|
||||
Expect<Equal<ExtractSerializedRelationProperties<object>, never>>,
|
||||
|
||||
// Union types distribute correctly
|
||||
Expect<
|
||||
Equal<
|
||||
ExtractSerializedRelationProperties<
|
||||
{ a: SerializedRelation } | { b: SerializedRelation }
|
||||
>,
|
||||
'a' | 'b'
|
||||
>
|
||||
>,
|
||||
|
||||
// Mixed union with object and primitive
|
||||
Expect<
|
||||
Equal<
|
||||
ExtractSerializedRelationProperties<{ rel: SerializedRelation } | string>,
|
||||
'rel'
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
|
@ -55,54 +55,39 @@ export type { EnumFieldMetadataType } from './EnumFieldMetadataType';
|
|||
export type { ExcludeFunctions } from './ExcludeFunctions';
|
||||
export type { ExtractPropertiesThatEndsWithId } from './ExtractPropertiesThatEndsWithId';
|
||||
export type { ExtractPropertiesThatEndsWithIds } from './ExtractPropertiesThatEndsWithIds';
|
||||
export type { ExtractSerializedRelationProperties } from './ExtractSerializedRelationProperties.type';
|
||||
export type {
|
||||
FieldMetadataDefaultValueFunctionNames,
|
||||
FieldMetadataClassValidation,
|
||||
FieldMetadataFunctionDefaultValue,
|
||||
FieldMetadataDefaultValueForType,
|
||||
FieldMetadataDefaultValueForAnyType,
|
||||
FieldMetadataDefaultValue,
|
||||
FieldMetadataDefaultSerializableValue,
|
||||
} from './FieldMetadataDefaultValue';
|
||||
export {
|
||||
fieldMetadataDefaultValueFunctionName,
|
||||
FieldMetadataDefaultValueString,
|
||||
FieldMetadataDefaultValueRawJson,
|
||||
FieldMetadataDefaultValueRichTextV2,
|
||||
FieldMetadataDefaultValueRichText,
|
||||
FieldMetadataDefaultValueNumber,
|
||||
FieldMetadataDefaultValueBoolean,
|
||||
FieldMetadataDefaultValueStringArray,
|
||||
FieldMetadataDefaultValueDateTime,
|
||||
FieldMetadataDefaultValueDate,
|
||||
FieldMetadataDefaultValueCurrency,
|
||||
FieldMetadataDefaultValueFullName,
|
||||
FieldMetadataDefaultValueUuidFunction,
|
||||
FieldMetadataDefaultValueNowFunction,
|
||||
FieldMetadataDefaultValueRichTextV2,
|
||||
FieldMetadataDefaultValueCurrency,
|
||||
FieldMetadataDefaultValueFullName,
|
||||
FieldMetadataDefaultValueAddress,
|
||||
FieldMetadataDefaultValueLinks,
|
||||
FieldMetadataDefaultActor,
|
||||
FieldMetadataDefaultValueEmails,
|
||||
FieldMetadataDefaultValuePhones,
|
||||
FieldMetadataDefaultArray,
|
||||
FieldMetadataDefaultValueMapping,
|
||||
FieldMetadataFunctionDefaultValue,
|
||||
FieldMetadataDefaultValueForAnyType,
|
||||
FieldMetadataDefaultValue,
|
||||
} from './FieldMetadataDefaultValue';
|
||||
export { fieldMetadataDefaultValueFunctionName } from './FieldMetadataDefaultValue';
|
||||
export type { FieldMetadataMultiItemSettings } from './FieldMetadataMultiItemSettings';
|
||||
export { FieldMetadataSettingsOnClickAction } from './FieldMetadataMultiItemSettings';
|
||||
export type { TagColor, FieldMetadataOptions } from './FieldMetadataOptions';
|
||||
export type {
|
||||
TagColor,
|
||||
FieldMetadataOptionForAnyType,
|
||||
FieldMetadataOptions,
|
||||
} from './FieldMetadataOptions';
|
||||
export {
|
||||
FieldMetadataDefaultOption,
|
||||
FieldMetadataComplexOption,
|
||||
} from './FieldMetadataOptions';
|
||||
export type {
|
||||
FieldNumberVariant,
|
||||
FieldMetadataNumberSettings,
|
||||
FieldMetadataTextSettings,
|
||||
FieldMetadataDateSettings,
|
||||
FieldMetadataDateTimeSettings,
|
||||
FieldMetadataRelationSettings,
|
||||
FieldMetadataAddressSettings,
|
||||
FieldMetadataFilesSettings,
|
||||
FieldMetadataTsVectorSettings,
|
||||
FieldMetadataSettingsMapping,
|
||||
AllFieldMetadataSettings,
|
||||
FieldMetadataSettings,
|
||||
} from './FieldMetadataSettings';
|
||||
|
|
@ -200,6 +185,8 @@ export type {
|
|||
RelationPredicateValue,
|
||||
RowLevelPermissionPredicateValue,
|
||||
} from './RowLevelPermissionPredicateValue';
|
||||
export type { SerializedRelation } from './SerializedRelation.type';
|
||||
export { SERIALIZED_RELATION_BRAND } from './SerializedRelation.type';
|
||||
export type { ServerlessFunctionEvent } from './ServerlessFunctionEvent';
|
||||
export { SettingsPath } from './SettingsPath';
|
||||
export type { Sources } from './SourcesType';
|
||||
|
|
|
|||
Loading…
Reference in a new issue