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

Add Guessers based on the OpenAPI Schema #80

Merged
merged 31 commits into from
Nov 7, 2024
Merged

Add Guessers based on the OpenAPI Schema #80

merged 31 commits into from
Nov 7, 2024

Conversation

fzaninotto
Copy link
Member

@fzaninotto fzaninotto commented Nov 5, 2024

Problem

React-admin's guessers fetch data and inspect it to scaffold CRUD views for a resource.

But there is no CreateGuesser, and the guessers sometimes make wrong assumptions.

Solution

Supabase uses PostgREST, which exposes an OpenAPI schema.

Provide alternative guessers that read this schema instead of fetching data.

Zero-config admin:

import { AdminGuesser } from 'ra-supabase';

const App = () => (
   <AdminGuesser
        apiUrl={import.meta.env.VITE_SUPABASE_URL}
        apiKey={import.meta.env.VITE_SUPABASE_ANON_KEY}
    />
);

Guessed admin:

import { Admin, Resource, CustomRoutes } from 'react-admin';
import { BrowserRouter, Route } from 'react-router-dom';
import { createClient } from '@supabase/supabase-js';
import {
    CreateGuesser,
    EditGuesser,
    ForgotPasswordPage,
    ListGuesser,
    LoginPage,
    SetPasswordPage,
    ShowGuesser,
    defaultI18nProvider,
    supabaseDataProvider,
    supabaseAuthProvider
} from 'ra-supabase';   

const instanceUrl = YOUR_SUPABASE_URL;
const apiKey = YOUR_SUPABASE_API_KEY;
const supabaseClient = createClient(instanceUrl, apiKey);
const dataProvider = supabaseDataProvider({ instanceUrl, apiKey, supabaseClient });
const authProvider = supabaseAuthProvider(supabaseClient, {});

export const App = () => (
    <BrowserRouter>
        <Admin
            dataProvider={dataProvider}
            authProvider={authProvider}
            i18nProvider={defaultI18nProvider}
            loginPage={LoginPage}
        >
            <Resource name="companies" list={ListGuesser} edit={EditGuesser} create={CreateGuesser} show={ShowGuesser} />
            <Resource name="contacts" list={ListGuesser} edit={EditGuesser} create={CreateGuesser} show={ShowGuesser} />
            <Resource name="deals" list={ListGuesser} edit={EditGuesser} create={CreateGuesser} show={ShowGuesser} />
            <Resource name="tags" list={ListGuesser} edit={EditGuesser} create={CreateGuesser} show={ShowGuesser} />
            <Resource name="tasks" list={ListGuesser} edit={EditGuesser} create={CreateGuesser} show={ShowGuesser} />
            <Resource name="dealNotes" list={ListGuesser} edit={EditGuesser} create={CreateGuesser} show={ShowGuesser} />
            <Resource name="contactNotes" list={ListGuesser} edit={EditGuesser} create={CreateGuesser} show={ShowGuesser} />
            <Resource name="sales" list={ListGuesser} edit={EditGuesser} create={CreateGuesser} show={ShowGuesser} />
            <CustomRoutes noLayout>
                <Route path={SetPasswordPage.path} element={<SetPasswordPage />} />
                <Route path={ForgotPasswordPage.path} element={<ForgotPasswordPage />} />
            </CustomRoutes>
        </Admin>
    </BrowserRouter>
);

Guessed Company list:

import { Datagrid, DateField, DateInput, List, NumberField, NumberInput, ReferenceField, ReferenceInput, TextField, TextInput, UrlField } from 'react-admin';

const filters = [
    <TextInput source="id" />,
    <TextInput source="name" />,
    <TextInput source="logo" />,
    <TextInput source="sector" />,
    <NumberInput source="size" />,
    <TextInput source="linkedIn" />,
    <TextInput source="website" />,
    <TextInput source="phone_number" />,
    <TextInput source="address" />,
    <TextInput source="zipcode" />,
    <TextInput source="city" />,
    <TextInput source="stateAbbr" />,
    <ReferenceInput source="sales_id" reference="sales" />,
    <DateInput source="created_at" />,
    <TextInput source="fts" />
];

export const CompanyList = () => (
    <List filters={filters}>
        <Datagrid>
            <TextField source="id" />
            <TextField source="name" />
            <TextField source="logo" />
            <TextField source="sector" />
            <NumberField source="size" />
            <TextField source="linkedIn" />
            <UrlField source="website" />
            <TextField source="phone_number" />
            <TextField source="address" />
            <TextField source="zipcode" />
            <TextField source="city" />
            <TextField source="stateAbbr" />
            <ReferenceField source="sales_id" reference="sales" />
            <DateField source="created_at" />
            <TextField source="fts" />
        </Datagrid>
    </List>
);

To do

  • Add dataProvider.getSchema() for schema introspection
  • Add useAPISchema hook to access is from React components
  • Create an alternative ListGuesser leveraging OpenAPI, and guessing filters, too
  • Create an alternative ShowGuesser leveraging OpenAPI
  • Create an alternative EditGuesser leveraging OpenAPI
  • Create an alternative CreateGuesser leveraging OpenAPI
  • Create an AdminGuesser leveraging OpenAPI
  • Detect dates in fields
  • Document it
  • Test it

@fzaninotto fzaninotto added the WIP label Nov 5, 2024
@fzaninotto fzaninotto added RFR and removed WIP labels Nov 6, 2024
packages/ra-supabase-core/package.json Outdated Show resolved Hide resolved
Comment on lines +45 to +46
supabaseClient: (url: string, options?: any) =>
httpClient(`${config.apiUrl}/${url}`, options),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where is this used?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is not, but I initially wanted to add the getSchema method directly in the demo app, only to realize I was missing the ability to call the API. This will prove very useful for custom endpoints.

packages/ra-supabase-core/src/useAPISchema.ts Outdated Show resolved Hide resolved
Comment on lines 55 to 92
React.useEffect(() => {
if (isPending || error) {
return;
}
const resourceDefinition = schema.definitions?.[resource];
const requiredFields = resourceDefinition?.required || [];
if (!resourceDefinition || !resourceDefinition.properties) {
throw new Error(
`The resource ${resource} is not defined in the API schema`
);
}
if (!resourceDefinition || !resourceDefinition.properties) {
return;
}
const inferredInputs = Object.keys(resourceDefinition.properties)
.filter((source: string) => source !== 'id')
.map((source: string) =>
inferElementFromType({
name: source,
types: editFieldTypes,
description:
resourceDefinition.properties![source].description,
format: resourceDefinition.properties![source].format,
type: (resourceDefinition.properties &&
resourceDefinition.properties[source] &&
typeof resourceDefinition.properties[source].type ===
'string'
? resourceDefinition.properties![source].type
: 'string') as string,
requiredFields,
})
);
const inferredForm = new InferredElement(
editFieldTypes.form,
null,
inferredInputs
);
setChild(inferredForm.getElement());
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should probably be a hook reusable between views

Copy link
Member Author

Choose a reason for hiding this comment

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

I disagree. This refactoring is not straightforward as we can only mutualize part of the effect (the console.log is different), and it has to interface with a state defined outside. I assume the refactoring you suggest will produce code that is harder to read (and the current code is already hard to read).

@fzaninotto fzaninotto added this to the 3.3.0 milestone Nov 7, 2024
@slax57 slax57 self-requested a review November 7, 2024 10:02
djhi

This comment was marked as outdated.

Comment on lines 59 to 95
const resourceDefinition = schema.definitions?.[resource];
const requiredFields = resourceDefinition?.required || [];
if (!resourceDefinition || !resourceDefinition.properties) {
throw new Error(
`The resource ${resource} is not defined in the API schema`
);
}
if (!resourceDefinition || !resourceDefinition.properties) {
return;
}
const inferredInputs = Object.keys(resourceDefinition.properties)
.filter((source: string) => source !== 'id')
.filter(
source =>
resourceDefinition.properties![source].format !== 'tsvector'
)
.map((source: string) =>
inferElementFromType({
name: source,
types: editFieldTypes,
description:
resourceDefinition.properties![source].description,
format: resourceDefinition.properties![source].format,
type: (resourceDefinition.properties &&
resourceDefinition.properties[source] &&
typeof resourceDefinition.properties[source].type ===
'string'
? resourceDefinition.properties![source].type
: 'string') as string,
requiredFields,
})
);
const inferredForm = new InferredElement(
editFieldTypes.form,
null,
inferredInputs
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry to insist but this could be extracted at least as a getInferredView function taking the following parameters:

  • types: here it would be editFieldTypes, listFieldTypes in ListGuesser
  • containerType: here editFieldTypes.form, listFieldTypes.table in ListGuesser

That would make it a lot easier to read and avoid a lot of duplication. It could also be a hook but that may hurt comprehension of the code

Copy link
Member Author

Choose a reason for hiding this comment

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

The code is different in each guesser. Sometimes we include requiredFields, and sometimes not. Sometimes we consider id fields, and sometimes not. Plus, the names of the variables are more readable (inferredFoem, inferredInput) than any abstract counterpart.

Comment on lines +120 to +124
<Resource name="${def.name}"${
def.list ? ' list={ListGuesser}' : ''
}${def.edit ? ' edit={EditGuesser}' : ''}${
def.create ? ' create={CreateGuesser}' : ''
}${def.show ? ' show={ShowGuesser}' : ''} />`
Copy link
Contributor

Choose a reason for hiding this comment

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

question: Contact and Sale default recordRepresentation are wrong. Do you think it would be possible to use the OpenAPI schema to fix them?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think the OpenAPI spec allows for that. I've seen nothing related in the default PostgREST OpenAPI output, nor in the PostgREST OpenAPI doc.

Copy link
Contributor

@slax57 slax57 left a comment

Choose a reason for hiding this comment

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

💪

@djhi djhi merged commit c9578ec into main Nov 7, 2024
5 checks passed
@djhi djhi deleted the openAPI-guessers branch November 7, 2024 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants