Merge pull request #54 from twentyhq/sammy/t-107-i-see-a-people-data-model-and-graphql

feature: add person schema in hasura
This commit is contained in:
Sammy Teillet
2023-04-20 14:55:01 +02:00
committed by GitHub
47 changed files with 456 additions and 191 deletions

View File

@ -1 +1 @@
REACT_APP_API_URL=http://localhost:3000
REACT_APP_API_URL=http://localhost:8080

View File

@ -31,6 +31,11 @@ const StyledClickable = styled.div`
:hover::before {
display: block;
}
a {
color: inherit;
text-decoration: none;
}
`;
const Container = styled.span`

View File

@ -10,9 +10,9 @@ import TableHeader from './table-header/TableHeader';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import styled from '@emotion/styled';
type OwnProps = {
data: Array<any>;
columns: Array<ColumnDef<any, any>>;
type OwnProps<TData> = {
data: Array<TData>;
columns: Array<ColumnDef<TData, any>>;
viewName: string;
viewIcon?: IconProp;
};
@ -60,7 +60,7 @@ const StyledTableWithHeader = styled.div`
flex: 1;
`;
function Table({ data, columns, viewName, viewIcon }: OwnProps) {
function Table<TData>({ data, columns, viewName, viewIcon }: OwnProps<TData>) {
const table = useReactTable({
data,
columns,

View File

@ -13,11 +13,13 @@ import { setContext } from '@apollo/client/link/context';
import '@emotion/react';
import { ThemeType } from './layout/styles/themes';
const httpLink = createHttpLink({ uri: process.env.REACT_APP_API_URL });
const httpLink = createHttpLink({
uri: `${process.env.REACT_APP_API_URL}/v1/graphql`,
});
const authLink = setContext((_, { headers }) => {
return {
headers: headers,
headers: { ...headers, 'x-hasura-admin-secret': 'secret' },
};
});

View File

@ -1,196 +1,52 @@
import {
faBuildings,
faCalendar,
faEnvelope,
faUser,
faMapPin,
faPhone,
faRectangleHistory,
faList,
} from '@fortawesome/pro-regular-svg-icons';
import { faUser, faList } from '@fortawesome/pro-regular-svg-icons';
import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
import Table from '../../components/table/Table';
import { Company } from '../../interfaces/company.interface';
import { Pipe } from '../../interfaces/pipe.interface';
import { createColumnHelper } from '@tanstack/react-table';
import styled from '@emotion/styled';
import ClickableCell from '../../components/table/ClickableCell';
import ColumnHead from '../../components/table/ColumnHead';
import { parsePhoneNumber, CountryCode } from 'libphonenumber-js';
import Checkbox from '../../components/form/Checkbox';
import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
import CompanyChip from '../../components/chips/CompanyChip';
import PersonChip from '../../components/chips/PersonChip';
type Person = {
fullName: string;
picture?: string;
email: string;
company: Company;
phone: string;
creationDate: Date;
pipe: Pipe;
city: string;
countryCode: string;
};
import { peopleColumns } from './people-table';
import { gql, useQuery } from '@apollo/client';
import { GraphqlPerson, Person } from './types';
import { defaultData } from './default-data';
import { mapPerson } from './mapper';
const StyledPeopleContainer = styled.div`
display: flex;
width: 100%;
`;
a {
color: inherit;
text-decoration: none;
export const GET_PEOPLE = gql`
query GetPeople {
person {
id
phone
email
city
firstname
lastname
created_at
company {
company_name
company_domain
}
}
}
`;
const defaultData: Array<Person> = [
{
fullName: 'Alexandre Prot',
picture: 'http://placekitten.com/256',
email: 'alexandre@qonto.com',
company: { id: 1, name: 'Qonto', domain: 'qonto.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 23, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
{
fullName: 'Alexandre Prot',
email: 'alexandre@qonto.com',
company: { id: 2, name: 'LinkedIn', domain: 'linkedin.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 23, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
{
fullName: 'Alexandre Prot',
picture: 'http://placekitten.com/256',
email: 'alexandre@qonto.com',
company: { id: 1, name: 'Qonto', domain: 'qonto.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 23, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
{
fullName: 'Alexandre Prot',
picture: 'https://placekitten.com/g/256',
email: 'alexandre@qonto.com',
company: { id: 1, name: 'Slack', domain: 'slack.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 23, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
{
fullName: 'Alexandre Prot',
email: 'alexandre@qonto.com',
company: { id: 2, name: 'Facebook', domain: 'facebook.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 23, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
];
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor('fullName', {
header: () => <ColumnHead viewName="People" viewIcon={faUser} />,
cell: (props) => (
<>
<HorizontalyAlignedContainer>
<Checkbox
id={`person-selected-${props.row.original.email}`}
name={`person-selected-${props.row.original.email}`}
/>
<PersonChip
name={props.row.original.fullName}
picture={props.row.original.picture}
/>
</HorizontalyAlignedContainer>
</>
),
}),
columnHelper.accessor('email', {
header: () => <ColumnHead viewName="Email" viewIcon={faEnvelope} />,
cell: (props) => (
<ClickableCell href={`mailto:${props.row.original.email}`}>
{props.row.original.email}
</ClickableCell>
),
}),
columnHelper.accessor('company', {
header: () => <ColumnHead viewName="Company" viewIcon={faBuildings} />,
cell: (props) => (
<ClickableCell href="#">
<CompanyChip
name={props.row.original.company.name}
picture={`https://www.google.com/s2/favicons?domain=${props.row.original.company.domain}&sz=256`}
/>
</ClickableCell>
),
}),
columnHelper.accessor('phone', {
header: () => <ColumnHead viewName="Phone" viewIcon={faPhone} />,
cell: (props) => (
<ClickableCell
href={parsePhoneNumber(
props.row.original.phone,
props.row.original.countryCode as CountryCode,
)?.getURI()}
>
{parsePhoneNumber(
props.row.original.phone,
props.row.original.countryCode as CountryCode,
)?.formatInternational() || props.row.original.phone}
</ClickableCell>
),
}),
columnHelper.accessor('creationDate', {
header: () => <ColumnHead viewName="Creation" viewIcon={faCalendar} />,
cell: (props) => (
<ClickableCell href="#">
{new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(props.row.original.creationDate)}
</ClickableCell>
),
}),
columnHelper.accessor('pipe', {
header: () => <ColumnHead viewName="Pipe" viewIcon={faRectangleHistory} />,
cell: (props) => (
<ClickableCell href="#">{props.row.original.pipe.name}</ClickableCell>
),
}),
columnHelper.accessor('city', {
header: () => <ColumnHead viewName="City" viewIcon={faMapPin} />,
cell: (props) => (
<ClickableCell href="#">{props.row.original.city}</ClickableCell>
),
}),
];
function People() {
const { data } = useQuery<{ person: GraphqlPerson[] }>(GET_PEOPLE);
const mydata: Person[] = data ? data.person.map(mapPerson) : defaultData;
return (
<WithTopBarContainer title="People" icon={faUser}>
<StyledPeopleContainer>
<Table
data={defaultData}
columns={columns}
viewName="All People"
viewIcon={faList}
/>
{mydata && (
<Table
data={mydata}
columns={peopleColumns}
viewName="All People"
viewIcon={faList}
/>
)}
</StyledPeopleContainer>
</WithTopBarContainer>
);

View File

@ -1,7 +1,9 @@
import { MemoryRouter } from 'react-router-dom';
import People from '../People';
import People, { GET_PEOPLE } from '../People';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { MockedProvider } from '@apollo/client/testing';
import { defaultData } from '../default-data';
const component = {
title: 'People',
@ -10,10 +12,25 @@ const component = {
export default component;
const mocks = [
{
request: {
query: GET_PEOPLE,
},
result: {
data: {
person: defaultData,
},
},
},
];
export const PeopleDefault = () => (
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<People />
</MemoryRouter>
</ThemeProvider>
<MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<People />
</MemoryRouter>
</ThemeProvider>
</MockedProvider>
);

View File

@ -0,0 +1,47 @@
import { Person } from './types';
export const defaultData: Array<Person> = [
{
fullName: 'Alexandre Prot',
picture: 'http://placekitten.com/256',
email: 'alexandre@qonto.com',
company: { id: 1, name: 'Qonto', domain: 'qonto.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 23, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
{
fullName: 'Alexandre Prot',
email: 'alexandre@qonto.com',
company: { id: 2, name: 'LinkedIn', domain: 'linkedin.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 22, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
{
fullName: 'Alexandre Prot',
picture: 'http://placekitten.com/256',
email: 'alexandre@qonto.com',
company: { id: 5, name: 'Sequoia', domain: 'sequoiacap.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 21, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
{
fullName: 'Alexandre Prot',
email: 'alexandre@qonto.com',
company: { id: 2, name: 'Facebook', domain: 'facebook.com' },
phone: '06 12 34 56 78',
creationDate: new Date('Feb 25, 2018'),
pipe: { id: 1, name: 'Sales Pipeline', icon: 'faUser' },
city: 'Paris',
countryCode: 'FR',
},
];

View File

@ -0,0 +1,14 @@
import { GraphqlPerson, Person } from './types';
export const mapPerson = (person: GraphqlPerson): Person => ({
fullName: `${person.firstname} ${person.lastname}`,
creationDate: new Date(person.created_at),
pipe: { name: 'coucou', id: 1, icon: 'faUser' },
...person,
company: {
id: 1,
name: person.company.company_name,
domain: person.company.company_domain,
},
countryCode: 'FR',
});

View File

@ -0,0 +1,98 @@
import {
faBuildings,
faCalendar,
faEnvelope,
faUser,
faMapPin,
faPhone,
faRectangleHistory,
} from '@fortawesome/pro-regular-svg-icons';
import { createColumnHelper } from '@tanstack/react-table';
import ClickableCell from '../../components/table/ClickableCell';
import ColumnHead from '../../components/table/ColumnHead';
import { parsePhoneNumber, CountryCode } from 'libphonenumber-js';
import Checkbox from '../../components/form/Checkbox';
import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
import CompanyChip from '../../components/chips/CompanyChip';
import PersonChip from '../../components/chips/PersonChip';
import { Person } from './types';
const columnHelper = createColumnHelper<Person>();
export const peopleColumns = [
columnHelper.accessor('fullName', {
header: () => <ColumnHead viewName="People" viewIcon={faUser} />,
cell: (props) => (
<>
<HorizontalyAlignedContainer>
<Checkbox
id={`person-selected-${props.row.original.email}`}
name={`person-selected-${props.row.original.email}`}
/>
<PersonChip
name={props.row.original.fullName}
picture={props.row.original.picture}
/>
</HorizontalyAlignedContainer>
</>
),
}),
columnHelper.accessor('email', {
header: () => <ColumnHead viewName="Email" viewIcon={faEnvelope} />,
cell: (props) => (
<ClickableCell href={`mailto:${props.row.original.email}`}>
{props.row.original.email}
</ClickableCell>
),
}),
columnHelper.accessor('company', {
header: () => <ColumnHead viewName="Company" viewIcon={faBuildings} />,
cell: (props) => (
<ClickableCell href="#">
<CompanyChip
name={props.row.original.company.name}
picture={`https://www.google.com/s2/favicons?domain=${props.row.original.company.domain}&sz=256`}
/>
</ClickableCell>
),
}),
columnHelper.accessor('phone', {
header: () => <ColumnHead viewName="Phone" viewIcon={faPhone} />,
cell: (props) => (
<ClickableCell
href={parsePhoneNumber(
props.row.original.phone,
props.row.original.countryCode as CountryCode,
)?.getURI()}
>
{parsePhoneNumber(
props.row.original.phone,
props.row.original.countryCode as CountryCode,
)?.formatInternational() || props.row.original.phone}
</ClickableCell>
),
}),
columnHelper.accessor('creationDate', {
header: () => <ColumnHead viewName="Creation" viewIcon={faCalendar} />,
cell: (props) => (
<ClickableCell href="#">
{new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(props.row.original.creationDate)}
</ClickableCell>
),
}),
columnHelper.accessor('pipe', {
header: () => <ColumnHead viewName="Pipe" viewIcon={faRectangleHistory} />,
cell: (props) => (
<ClickableCell href="#">{props.row.original.pipe.name}</ClickableCell>
),
}),
columnHelper.accessor('city', {
header: () => <ColumnHead viewName="City" viewIcon={faMapPin} />,
cell: (props) => (
<ClickableCell href="#">{props.row.original.city}</ClickableCell>
),
}),
];

View File

@ -0,0 +1,30 @@
import { Company } from '../../interfaces/company.interface';
import { Pipe } from '../../interfaces/pipe.interface';
export type Person = {
fullName: string;
picture?: string;
email: string;
company: Company;
phone: string;
creationDate: Date;
pipe: Pipe;
city: string;
countryCode: string;
};
export type GraphqlPerson = {
city: string;
company: {
__typename: string;
company_name: string;
company_domain: string;
};
created_at: string;
email: string;
firstname: string;
id: number;
lastname: string;
phone: string;
__typename: string;
};

View File

@ -0,0 +1,10 @@
table:
name: person
schema: public
object_relationships:
- name: company
using:
foreign_key_constraint_on: company_id
- name: workspace
using:
foreign_key_constraint_on: workspace_id

View File

@ -0,0 +1,3 @@
table:
name: company
schema: public

View File

@ -1 +1,3 @@
- "!include public_company.yaml"
- "!include public_person.yaml"
- "!include public_workspaces.yaml"

View File

@ -0,0 +1 @@
DROP TABLE "public"."person";

View File

@ -0,0 +1 @@
CREATE TABLE "public"."person" ("id" serial NOT NULL, "firstname" text, "lastname" text NOT NULL, "company_domain" text, "phone" text, "city" text, PRIMARY KEY ("id") , UNIQUE ("id"));

View File

@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."person" add column "workspace_id" integer
-- not null;

View File

@ -0,0 +1,2 @@
alter table "public"."person" add column "workspace_id" integer
not null;

View File

@ -0,0 +1 @@
alter table "public"."person" drop constraint "person_workspace_id_fkey";

View File

@ -0,0 +1,5 @@
alter table "public"."person"
add constraint "person_workspace_id_fkey"
foreign key ("workspace_id")
references "public"."workspaces"
("id") on update restrict on delete restrict;

View File

@ -0,0 +1 @@
DROP TABLE "public"."company";

View File

@ -0,0 +1 @@
CREATE TABLE "public"."company" ("id" serial NOT NULL, "company_name" text NOT NULL, "company_domain" text NOT NULL, "workspace_id" integer NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON UPDATE restrict ON DELETE restrict);

View File

@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."person" add column "company_id" integer
-- null;

View File

@ -0,0 +1,2 @@
alter table "public"."person" add column "company_id" integer
null;

View File

@ -0,0 +1 @@
alter table "public"."person" drop constraint "person_company_id_fkey";

View File

@ -0,0 +1,5 @@
alter table "public"."person"
add constraint "person_company_id_fkey"
foreign key ("company_id")
references "public"."company"
("id") on update restrict on delete restrict;

View File

@ -0,0 +1,2 @@
alter table "public"."person" alter column "company_domain" drop not null;
alter table "public"."person" add column "company_domain" text;

View File

@ -0,0 +1 @@
alter table "public"."person" drop column "company_domain" cascade;

View File

@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."person" add column "email" text
-- null;

View File

@ -0,0 +1,2 @@
alter table "public"."person" add column "email" text
null;

View File

@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."person" add column "created_at" timestamptz
-- null default now();

View File

@ -0,0 +1,2 @@
alter table "public"."person" add column "created_at" timestamptz
null default now();

View File

@ -0,0 +1,21 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."person" add column "updated_at" timestamptz
-- null default now();
--
-- CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-- RETURNS TRIGGER AS $$
-- DECLARE
-- _new record;
-- BEGIN
-- _new := NEW;
-- _new."updated_at" = NOW();
-- RETURN _new;
-- END;
-- $$ LANGUAGE plpgsql;
-- CREATE TRIGGER "set_public_person_updated_at"
-- BEFORE UPDATE ON "public"."person"
-- FOR EACH ROW
-- EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-- COMMENT ON TRIGGER "set_public_person_updated_at" ON "public"."person"
-- IS 'trigger to set value of column "updated_at" to current timestamp on row update';

View File

@ -0,0 +1,19 @@
alter table "public"."person" add column "updated_at" timestamptz
null default now();
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_person_updated_at"
BEFORE UPDATE ON "public"."person"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_person_updated_at" ON "public"."person"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';

View File

@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."company" add column "created_at" timestamptz
-- null default now();

View File

@ -0,0 +1,2 @@
alter table "public"."company" add column "created_at" timestamptz
null default now();

View File

@ -0,0 +1,21 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."company" add column "updated_at" timestamptz
-- not null default now();
--
-- CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-- RETURNS TRIGGER AS $$
-- DECLARE
-- _new record;
-- BEGIN
-- _new := NEW;
-- _new."updated_at" = NOW();
-- RETURN _new;
-- END;
-- $$ LANGUAGE plpgsql;
-- CREATE TRIGGER "set_public_company_updated_at"
-- BEFORE UPDATE ON "public"."company"
-- FOR EACH ROW
-- EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-- COMMENT ON TRIGGER "set_public_company_updated_at" ON "public"."company"
-- IS 'trigger to set value of column "updated_at" to current timestamp on row update';

View File

@ -0,0 +1,19 @@
alter table "public"."company" add column "updated_at" timestamptz
not null default now();
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_company_updated_at"
BEFORE UPDATE ON "public"."company"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_company_updated_at" ON "public"."company"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';

View File

@ -0,0 +1 @@
alter table "public"."company" alter column "created_at" drop not null;

View File

@ -0,0 +1 @@
alter table "public"."company" alter column "created_at" set not null;

View File

@ -0,0 +1 @@
alter table "public"."person" alter column "created_at" drop not null;

View File

@ -0,0 +1 @@
alter table "public"."person" alter column "created_at" set not null;

View File

@ -0,0 +1 @@
alter table "public"."person" alter column "updated_at" drop not null;

View File

@ -0,0 +1 @@
alter table "public"."person" alter column "updated_at" set not null;

View File

@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."workspaces" add column "created_at" timestamptz
-- not null default now();

View File

@ -0,0 +1,2 @@
alter table "public"."workspaces" add column "created_at" timestamptz
not null default now();

View File

@ -0,0 +1,21 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."workspaces" add column "updated_at" timestamptz
-- not null default now();
--
-- CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
-- RETURNS TRIGGER AS $$
-- DECLARE
-- _new record;
-- BEGIN
-- _new := NEW;
-- _new."updated_at" = NOW();
-- RETURN _new;
-- END;
-- $$ LANGUAGE plpgsql;
-- CREATE TRIGGER "set_public_workspaces_updated_at"
-- BEFORE UPDATE ON "public"."workspaces"
-- FOR EACH ROW
-- EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
-- COMMENT ON TRIGGER "set_public_workspaces_updated_at" ON "public"."workspaces"
-- IS 'trigger to set value of column "updated_at" to current timestamp on row update';

View File

@ -0,0 +1,19 @@
alter table "public"."workspaces" add column "updated_at" timestamptz
not null default now();
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_workspaces_updated_at"
BEFORE UPDATE ON "public"."workspaces"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_workspaces_updated_at" ON "public"."workspaces"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';