Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adding support for new FieldMetadataType with Postgres enums #2674

Merged
merged 7 commits into from
Nov 30, 2023

Conversation

magrinj
Copy link
Member

@magrinj magrinj commented Nov 23, 2023

Description

This PR introduces support for a new FieldMetadataType, utilizing Postgres enums to enhance our data modeling capabilities. We have introduced three new types: RATING, SELECT, and MULTI_SELECT.

Key features

  1. Creation and Update of FieldMetadata:

    • We can now create or update FieldMetadata with additional options for these enum types. For instance, when creating a field with the type MULTI_SELECT, the options include properties like position, color, label, and value.
    • Example GraphQL mutation for creating a FieldMetadata:
      mutation CreateOneField {
        createOneField(input: {
          field: {
            name: "multiSelect", 
            label: "Multi Select", 
            type: MULTI_SELECT, 
            objectMetadataId: "0053a033-e2a7-44c6-a2ab-34437bc82c46", 
            options: [
              {position: 0, color: "#fff", label: "First", value: "MULTI_FIRST"}, 
              {position: 1, color: "#fff", label: "Second", value: "MULTI_SECOND"}
            ]
          }
        }) {
          id
          options
        }
      }
  2. Options update

    • The backend generates a unique id for each option within a FieldMetadata type. This id is crucial for the updateOneField mutation, ensuring a seamless transition between old and new enum values.
  3. Utilization in Document queries:

    • When a field is established, it can now include the appropriate enum value. Here are examples of mutations for inserting enum values into a document:
      mutation MyMutation {
        createDocument(data: {multiSelect: [MULTI_FIRST], name: "MultiFirst"}) {
          id
          name
          multiSelect
          createdAt
          deletedAt
          updatedAt
        }
      }
      Or
      mutation MyMutation {
        createDocument(data: {select: FIRST, name: "First"}) {
          id
          name
          select
          createdAt
          deletedAt
          updatedAt
        }
      }
  4. Frontend Integration:

    • The frontend is required to map FieldMetadata options with the enum values. This mapping will deduce properties such as color and position, facilitating the way enums are updated in all the views.

Known issues

Filtering on multiple choices enum doesn't work, it's due to pg_graphql that is not handling them properly supabase/pg_graphql#458

Copy link

github-actions bot commented Nov 29, 2023

TODOs/FIXMEs:

  • // TODO: Add validation for this but we don't have the type actually: server/src/metadata/field-metadata/dtos/update-field.input.ts

Generated by 🚫 dangerJS against 75054de

@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporary workaround as composite type doesn't use the correct type in database, TEXT should be text not varchar.
cc @Weiko

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, we will need to migrate our DB though (to change the metadata). cc @charlesBochet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a temporary workaround, maybe we should actually use TEXT and run the migration to have the final result directly? (which is having text in the workspace schema I guess?). Except if we want to handle SHORT_TEXT type or something and use varchar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh, I'm pretty sure varchar is the same as text for postgres if you don't define a particular size

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes see here

The TEXT data type can store a string with unlimited length.

If you do not specify the n integer for the VARCHAR data type, it behaves like the TEXT datatype. The performance of the VARCHAR (without the size n) and TEXT are the same.

https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-char-varchar-text/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to store currency in an enum in the future?
Plus: data is more robust
Minus: migration when geopolitics evolves :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding varchar vs text, I don't think we should use both. I think varchar should be good, and agree we should avoid generating migrations if possible

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So not sure to understand the motivation behind this change

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Weiko: Ok good point that Postgres handle varchar without length in the same way as text.
I'm ok with avoiding migrations, but I don't really like to have inconsistence between the metadata and what is actually stored in the database.

For better explanation @charlesBochet, instead of always redefining the composite types like Link or Currency, I've moved the composite types definitions that initially was in the schema-builder into the field-metadata.
That way we're using the same object to define them. But it involve creating the FieldMetadata for the inner fields of composite types, and that was manually done before by manually putting the type varchar.
But actually varchar is not mapped in our FieldMetadataType.
The first thing I does is putting the type FieldMetadataType.TEXT for this fields, but this one is converted to a text column type in Postgres not a varchar.
It's why I've created a temporary type to avoid breaking changes, but from what @Weiko says, I can still put TEXT, we'll just have some inconsistency but it should work the same.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to store currency in an enum in the future? Plus: data is more robust Minus: migration when geopolitics evolves :D

It's something that we can consider

) {
const enumValues = migrationColumn.enum;

// TODO: Maybe we can do something better if we can recreate the old `TableColumn` object
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to rely on TypeORM by recreating the old TableColumn.
This is needed to support updating other properties at the same time.
In this PR we can't change the options of a FieldMetadata with the defaultValue at the same time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate more here? What happens if we do both at same time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we currently do both at the same time, TypeORM migrations will brakes enum migrations an drop the enum column before recreating it, so all the data will be lost

@magrinj magrinj changed the title Feat/enums feat: Adding support for new FieldMetadataType with Postgres enums Nov 29, 2023
@magrinj magrinj marked this pull request as ready for review November 29, 2023 14:23
@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, we will need to migrate our DB though (to change the metadata). cc @charlesBochet

@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a temporary workaround, maybe we should actually use TEXT and run the migration to have the final result directly? (which is having text in the workspace schema I guess?). Except if we want to handle SHORT_TEXT type or something and use varchar

@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh, I'm pretty sure varchar is the same as text for postgres if you don't define a particular size

@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes see here

The TEXT data type can store a string with unlimited length.

If you do not specify the n integer for the VARCHAR data type, it behaves like the TEXT datatype. The performance of the VARCHAR (without the size n) and TEXT are the same.

https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-char-varchar-text/

@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So instead maybe we can keep varchar, that would avoid unnecessary migrations @magrinj @charlesBochet
Ideally we should keep things consistent but looks like in this case it's fine

} from 'src/metadata/workspace-migration/workspace-migration.entity';
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';

export class AbstractFactory<T extends FieldMetadataType | 'default'>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a less generic name instead of AbstractFactory? WorkspaceMigrationFactory or something like that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Weiko Yes we can, but actually AbstractFactory should only be used inside this folder.
I'll rename it WorkspaceColumnActionAbstractFactory

import { WorkspaceMigrationColumnAlter } from 'src/metadata/workspace-migration/workspace-migration.entity';

@Injectable()
export class WorkspaceMigrationEnumService {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we prefix custom enums with "_"? @charlesBochet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will because the name is based on: ${tableName}_${migrationColumn.columnName}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean because of table name? What if we add a custom enum on a standard object? As for the column name then yes we just to make sure we put an _ there when creating the migration

Copy link
Member Author

@magrinj magrinj Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Weiko Actually enum names are based on the field name, so if the field is _myField the enum will be something like _myTable__myField_enum

Copy link
Member

@charlesBochet charlesBochet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another masterpiece!

@@ -3,7 +3,7 @@ ARG PG_MAIN_VERSION=14
FROM postgres:${PG_MAIN_VERSION} as postgres

ARG PG_MAIN_VERSION
ARG PG_GRAPHQL_VERSION=1.3.0
ARG PG_GRAPHQL_VERSION=1.4.2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will also need to update our local install script

@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to store currency in an enum in the future?
Plus: data is more robust
Minus: migration when geopolitics evolves :D

@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding varchar vs text, I don't think we should use both. I think varchar should be good, and agree we should avoid generating migrations if possible

@@ -22,7 +22,7 @@ export const currencyObjectDefinition = {
} satisfies FieldMetadataInterface,
{
id: 'currencyCode',
type: FieldMetadataType.TEXT,
type: FieldMetadataType.VARCHAR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So not sure to understand the motivation behind this change

const updatedFieldMetadata = await super.updateOne(id, record);

if (record.options || record.defaultValue) {
await this.workspaceMigrationService.createCustomMigration(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about the createCustomMigration API long term. I feel that we should maybe provide something more restrictive: createColumn, udpateColumnType, updateEnumOptions...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can let it like this for now

import { WorkspaceMigrationColumnAlter } from 'src/metadata/workspace-migration/workspace-migration.entity';

@Injectable()
export class WorkspaceMigrationEnumService {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will because the name is based on: ${tableName}_${migrationColumn.columnName}

);

// Update existing rows to handle missing values
await this.handleMissingEnumValues(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the first migration that will touch the data. I am very curious to see how it behaves on production.
On a regular project I would avoid that, but as we are doing migration per workspace, it's actually great. It will help migrating between versions of twenty handling data migration too and we will be able to test it on test workspaces, tier 3 workspaces before trying it on more important workspaces or clients!

);
}

private async renameEnumType(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so much complexity, but all steps make sense to me

) {
const enumValues = migrationColumn.enum;

// TODO: Maybe we can do something better if we can recreate the old `TableColumn` object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate more here? What happens if we do both at same time?

@magrinj magrinj merged commit 6e6f0af into main Nov 30, 2023
8 of 13 checks passed
@magrinj magrinj deleted the feat/enums branch November 30, 2023 14:24
@charlesBochet charlesBochet mentioned this pull request Dec 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: add new RATING type feat: support SELECT type on per tenant back-end
3 participants