conventional-tan
conventional-tanβ€’17mo ago

Composite Primary Keys recommendations

Hey Refine-Team, hello Discord! Thank you for refine, it's a really good solution and has provided a lot of "wow" effects for the people I work with over the last couple of weeks. We are building a CRUD-UI using Refine for some pretty large databases, spanning 9 dbs over multiple hundred tables. We have a specification that is asking for composite-primary-key (CPK) access. Meaning I have an API that is offering access to a resource like this /post/:id/:locale_id/:provider_id. All keys must be provided for the CPK to access the correct resource. I found the BaseRecord and BaseKey interface I'd probably override?
export type BaseKey = string | number;
export type BaseRecord = {
id?: BaseKey;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
export type BaseKey = string | number;
export type BaseRecord = {
id?: BaseKey;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
I guess I will have to write another dataProvider with extended functionality for this. Maybe someone has done something similar already, any help or hint is appreciated!
30 Replies
kapa.ai
kapa.aiβ€’17mo ago
Hi @bastianwegge. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
kapa.ai
kapa.aiβ€’17mo ago
---------------------- Hello! I'm glad to hear that refine has been helpful for your projects. Regarding your question about composite-primary-key (CPK) access, you're on the right track with the idea of creating a custom dataProvider to handle this specific use case. Unfortunately, the information extracts I have don't provide a direct example of implementing a dataProvider for composite-primary-keys. However, you can refer to the documentation on dataProvider (https://refine.dev/docs/api-reference/core/providers/data-provider/) to understand how to create a custom dataProvider and adapt it to your specific needs. When creating your custom dataProvider, you'll need to implement the necessary methods (e.g., getList, getOne, create, update, deleteOne, etc.) to handle the composite-primary-keys in the API requests. You can use the BaseRecord and BaseKey interfaces as a starting point and extend them as needed. I hope this helps you get started on implementing the custom dataProvider for your project. Good luck! ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
Data Provider | refine
TheΒ data providerΒ acts as a data layer for your app that makes the HTTP requests and encapsulates how the data is retrieved. refine consumes these methods via data hooks.
kapa.ai
kapa.aiβ€’17mo ago
<@317433576721219584> kapa.ai is still learning and improving, please let us how it did by reacting below
Omer
Omerβ€’17mo ago
Hey @bastianwegge πŸ‘‹ , Wow, greetings to the team that brings beauty to our day! My suggestion would have been similar, I believe that you can access what you want by using a custom dataProvider and meta (https://refine.dev/docs/tutorial/understanding-dataprovider/create-dataprovider/#meta-usage). If you encounter any issues during implementation, please don't hesitate to ask questions. Helping you would make us happy.
conventional-tan
conventional-tanβ€’17mo ago
@Omer so you would use :locale_id and :provider_id in meta ... πŸ€” These aren't (unfortunately) things I could provide through something like a HOC or Context. It's rather a Composite-Primary-Key for the list that comes back from the server. That's why I thought I'd adjust the BaseKey property, which I will try now. I haven't gotten into the details yet and I'm not very deep in refine's implementation. So I get a list with: - id: 5, locale_id: 1, provider_id: 2 - id: 5, locale_id: 2, provider_id: 3 - id: 5, locale_id: 3, provider_id: 4 When I click on "view" on the first element I want to have a route like: - /post/:id/:locale_id/:provider_id - so /post/5/1/2 If that makes sense at all πŸ˜… Before anybody's answering, Γ–mer you were completely right ! πŸš€
Omer
Omerβ€’17mo ago
We support the use of 'meta' within the routes, but it has not yet been documented. @aliemirs can provide us with some information
conventional-tan
conventional-tanβ€’17mo ago
There's a hint that resources can take meta here: https://refine.dev/docs/tutorial/understanding-resources/index/#defining-actions-for-a-resource)
{
name: "blog_posts",
edit: "/blog-posts/edit/:id/:version",
}
{
name: "blog_posts",
edit: "/blog-posts/edit/:id/:version",
}
But how am I supposed to give meta to this object?
{
name: "blog_posts",
edit: "/blog-posts/edit/:id/:version",
meta: {
locale_id: 'locale_id',
provider_id: 'provider_id',
}
}
{
name: "blog_posts",
edit: "/blog-posts/edit/:id/:version",
meta: {
locale_id: 'locale_id',
provider_id: 'provider_id',
}
}
The solution above does not seem to work. I found out though that I can pass meta indirectly to the buttons (I'm using antd) like this:
<ShowButton
hideText
size="small"
recordItemId={record.id}
meta={{ provider_id: record.provider_id, locale_id: record.locale_id }}
/>
<ShowButton
hideText
size="small"
recordItemId={record.id}
meta={{ provider_id: record.provider_id, locale_id: record.locale_id }}
/>
Yeah @Omer that's what I'm stumbling across right now I guess πŸ˜„
Omer
Omerβ€’17mo ago
Wow, you have already discovered the 'meta' usage that I was referring to, haha I am really impressed 🍻
conventional-tan
conventional-tanβ€’17mo ago
By accident I guess :D. The button shows the correct link and the router resolves the correct route. But when I'm accessing that link, the dataProvider does not get the meta information I referenced.
Omer
Omerβ€’17mo ago
I think we are not passing the resource 'meta' to the data providers. For now, you should do this manually, but if you create a GitHub Feat issue, we can prioritize the automatic transfer of the 'meta' information.
conventional-tan
conventional-tanβ€’17mo ago
Do you mean passing the same meta to useTable? Or to useOne ? useOne Meta usage is mentioned in the docs, I tried to get to the Show page using the inferencer (because I'm lazy), but that didn't work unfortunately πŸ˜„
Omer
Omerβ€’17mo ago
Yes, definitely. It makes sense to transfer everything that comes from useParsed or exists in the resource meta
conventional-tan
conventional-tanβ€’17mo ago
Alright, I'm gonna try that. Thanks a lot!
Omer
Omerβ€’17mo ago
Ah, yes, you cannot do this with the inferencer 😦 Creating pages seems like the most logical option.
conventional-tan
conventional-tanβ€’17mo ago
If only I wasn't lazy πŸ™ˆ
Omer
Omerβ€’17mo ago
It's not your fault. The inferencer is so good that it makes people lazy πŸ˜›
conventional-tan
conventional-tanβ€’17mo ago
It works πŸ’ͺ ! 1. make the single-resource available from the "list" (used inferrer here, see above) 2. create the component for showing:
import { DateField, NumberField, Show, TagField, TextField } from '@refinedev/antd';
import { IResourceComponentsProps, useOne, useParsed, useShow } from '@refinedev/core';
import { Typography } from 'antd';
import React from 'react';

const { Title } = Typography;


export const BlogI18nShow: React.FC<IResourceComponentsProps> = () => {
const parsedParams = useParsed();
const { locale_id, provider_id } = parsedParams.params as any;
console.log({ locale_id, provider_id });

const { queryResult } = useShow({ meta: { locale_id, provider_id } });
const { data, isLoading } = queryResult;

const record = data?.data;

const { data: localeData, isLoading: localeIsLoading } = useOne({
resource: 'locales',
id: record?.locale_id || '',
queryOptions: {
enabled: !!record,
},
});

const { data: providerData, isLoading: providerIsLoading } = useOne({
resource: 'providers',
id: record?.provider_id || '',
queryOptions: {
enabled: !!record,
},
});

return (
<Show isLoading={isLoading}>
<Title level={5}>Id</Title>
<NumberField value={record?.id ?? ''} />
<Title level={5}>Name</Title>
<TextField value={record?.name} />
<Title level={5}>Locale</Title>
{localeIsLoading ? <>Loading...</> : <>{localeData?.data?.name}</>}
<Title level={5}>Provider</Title>
{providerIsLoading ? <>Loading...</> : <>{providerData?.data?.name}</>}
</Show>
);
};
import { DateField, NumberField, Show, TagField, TextField } from '@refinedev/antd';
import { IResourceComponentsProps, useOne, useParsed, useShow } from '@refinedev/core';
import { Typography } from 'antd';
import React from 'react';

const { Title } = Typography;


export const BlogI18nShow: React.FC<IResourceComponentsProps> = () => {
const parsedParams = useParsed();
const { locale_id, provider_id } = parsedParams.params as any;
console.log({ locale_id, provider_id });

const { queryResult } = useShow({ meta: { locale_id, provider_id } });
const { data, isLoading } = queryResult;

const record = data?.data;

const { data: localeData, isLoading: localeIsLoading } = useOne({
resource: 'locales',
id: record?.locale_id || '',
queryOptions: {
enabled: !!record,
},
});

const { data: providerData, isLoading: providerIsLoading } = useOne({
resource: 'providers',
id: record?.provider_id || '',
queryOptions: {
enabled: !!record,
},
});

return (
<Show isLoading={isLoading}>
<Title level={5}>Id</Title>
<NumberField value={record?.id ?? ''} />
<Title level={5}>Name</Title>
<TextField value={record?.name} />
<Title level={5}>Locale</Title>
{localeIsLoading ? <>Loading...</> : <>{localeData?.data?.name}</>}
<Title level={5}>Provider</Title>
{providerIsLoading ? <>Loading...</> : <>{providerData?.data?.name}</>}
</Show>
);
};
3. adjust the data-provider to take the meta-fields we just passed in
...
getOne: async ({ resource, id, meta }) => {
console.log({ resource, id, meta });

let url = `${apiUrl}/${resource}/${id}`;
if (meta && meta.hasOwnProperty('locale_id') && meta.hasOwnProperty('provider_id')) {
url = `${url}/${meta.locale_id}/${meta.provider_id}`;
}
...
...
getOne: async ({ resource, id, meta }) => {
console.log({ resource, id, meta });

let url = `${apiUrl}/${resource}/${id}`;
if (meta && meta.hasOwnProperty('locale_id') && meta.hasOwnProperty('provider_id')) {
url = `${url}/${meta.locale_id}/${meta.provider_id}`;
}
...
@Omer is there a way to properly cast the route-context-bindings that create the routeParams? Usually (with router v6) you can useParams<{ x: string, y: string }> but there's no generic behavior on useParsed right now. Is that something that "is like it is right now"? Or is there a way I don't see to get properly typed Params?
Omer
Omerβ€’17mo ago
Nice catch. We forgot to add the Generic Type πŸ™‚ Could you create a GitHub issue for it? πŸ₯
conventional-tan
conventional-tanβ€’17mo ago
Will do right away, I will accept it in the current state for now. Thanks again a lot for your help!
Omer
Omerβ€’17mo ago
But still, you can also use react-router-dom's useParams instead of useParsed
conventional-tan
conventional-tanβ€’17mo ago
What would you recommend? 😁
Omer
Omerβ€’17mo ago
I think you should create an issue and use useParams (for type safety) until we implement it :p https://github.com/refinedev/refine/blob/next/packages/react-router-v6/src/bindings.tsx#L87
conventional-tan
conventional-tanβ€’9mo ago
Issue created with some docs and links: https://github.com/refinedev/refine/issues/4031 @Omer I'm reviving this thread, hope that's ok for you. @core this one's for you as well πŸ™‚ We're currently doing something like this as prepareUrl inside of our custom-data-provider.
const isTranslationTable = resource.toLowerCase().includes('i18n');
if (isTranslationTable && metaHasLocale && metaHasProvider) {
return `${url}/${id}/${meta.locale_id}/${meta.provider_id}`;
}
const isTranslationTable = resource.toLowerCase().includes('i18n');
if (isTranslationTable && metaHasLocale && metaHasProvider) {
return `${url}/${id}/${meta.locale_id}/${meta.provider_id}`;
}
I have three of theses and I'm about to implement the fourth, whereas I'm just trying to see: "Is there a meta field with the same name in the route of my resource? OK then set the param in the url to the meta-fields value!" Example for the resource:
...
{
name: 'x',
identifier: 'x',
list: '/x/',
show: '/x/show/:id/:locale_id/:provider_id',
edit: '/x/edit/:id/:locale_id/:provider_id',
},
...
...
{
name: 'x',
identifier: 'x',
list: '/x/',
show: '/x/show/:id/:locale_id/:provider_id',
edit: '/x/edit/:id/:locale_id/:provider_id',
},
...
So my question basically is: Is there a way to access the refine-resource (not just the name but the object that also has the route) inside of the data-provider? As the data-provider is handling the urls I find this to be a necessity. This way I could just create the URL from my actual resource without any tinkering. This would be a great addition to refine IMHO.
Omer
Omerβ€’9mo ago
Hey @bastianwegge , It's great to see you again! Would using the meta property in the resource definition be helpful for you? πŸ‘€ https://refine.dev/docs/guides-concepts/general-concepts/#meta-concept
General Concepts | refine
Refine is an extensible framework designed for rapidly building web applications. It offers a modern, hook-based architecture, a pluggable system of providers, and a robust state management solution. This section provides an overview of the key concepts in Refine.
conventional-tan
conventional-tanβ€’9mo ago
Could you specify your suggested usage? Would I do something like this?
...
{
name: 'x',
identifier: 'x',
list: '/x/',
show: '/x/show/:id/:locale_id/:provider_id',
edit: '/x/edit/:id/:locale_id/:provider_id',
meta: {
editUrl: '/x/edit/:id/:locale_id/:provider_id'
}
},
...
...
{
name: 'x',
identifier: 'x',
list: '/x/',
show: '/x/show/:id/:locale_id/:provider_id',
edit: '/x/edit/:id/:locale_id/:provider_id',
meta: {
editUrl: '/x/edit/:id/:locale_id/:provider_id'
}
},
...
Omer
Omerβ€’9mo ago
Yes, exactly like that, why not? πŸ˜›
conventional-tan
conventional-tanβ€’9mo ago
I'll try, thanks a lot for the immediate response! ❀️ Yes that works as a work-around! Thank you very much! Again I really think the resources' URL should be available inside of the data-provider. πŸ€”
Omer
Omerβ€’9mo ago
By the way, we have published our new guides. We would be really happy if you could review them and give us feedback when you have time 🍻 - https://refine.dev/docs/guides-concepts/general-concepts/ - https://refine.dev/docs/guides-concepts/data-fetching/ - https://refine.dev/docs/guides-concepts/forms/ - https://refine.dev/docs/guides-concepts/routing/ - https://refine.dev/docs/guides-concepts/authentication/ - https://refine.dev/docs/guides-concepts/authorization/ - https://refine.dev/docs/guides-concepts/ui-libraries/ Yep, can you create an issue? I'm sure someone from the community will definitely be interested πŸ”₯
conventional-tan
conventional-tanβ€’9mo ago
Of course I'm gonna have a look at the guides pages!
conventional-tan
conventional-tanβ€’9mo ago
@Omer I created an issue: https://github.com/refinedev/refine/issues/5306 I hope I got everything right.
GitHub
[FEAT] Provide resource information inside of the data-provider for...
Is your feature request related to a problem? Please describe. We're currently creating a workaround inside of our custom-data-provider to provide multi-key resource functionality. Say a URL no...