diff --git a/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md b/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md index d4055d461..b687f5d16 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md @@ -539,9 +539,9 @@ This way, when creating or editing a record you will be able to choose value for When foreign resource column is not required, selector will have an 'Unset' option that will set field to `null`. You can change label for this option using `unsetLabel`, like so: -```typescript title="./resources/adminuser.ts" +```typescript title="./resources/apartments.ts" export default { - name: 'adminuser', + name: 'apartments', columns: [ ... { @@ -558,6 +558,96 @@ export default { ], ``` +### Polymorphic foreign resources + +Sometimes it is needed for one column to be a foreign key for multiple tables. For example, given the following schema: + +```prisma title="./schema.prisma" +... +model apartments { + id String @id + created_at DateTime? + title String + square_meter Float? + price Decimal + number_of_rooms Int? + realtor_id String? +} + +model houses { + id String @id + created_at DateTime? + title String + house_square_meter Float? + land_square_meter Float? + price Decimal + realtor_id String? +} + +model sold_property { + id String @id + created_at DateTime? + title String + property_id String + realtor_id String? +} + +``` + +Here, in `sold_property` table, column `property_id` can be a foreign key for both `apartments` and `houses` tables. If schema is set like this, the is no way to tell to what table exactly `property_id` links to. Also, if defined like usual, adminforth will link to only one of them. To make sure that `property_id` works as intended we need add one more column to `sold_property` and change the way foreign resource is defined in adminforth resource config. + +```prisma title="./schema.prisma" +... + +model sold_property { + id String @id + created_at DateTime? + title String +//diff-add + property_type String + property_id String + realtor_id String? +} + +``` + +`property_type` column will be used to store what table id in `property_id` refers to. And in adminforth config for `sold_property` table, when describing `property_id` column, foreign resource field should be defined as follows: + +```typescript title="./resources/sold_property.ts" +export default { + name: 'sold_property', + columns: [ + ... + { + name: "property_type", + showIn: { create: false, edit: false }, + }, + { + name: "property_id", + foreignResource: { + polymorphicResources: [ + { + resourceId: 'apartments', + whenValue: 'apartment', + }, + { + resourceId: 'houses', + whenValue: 'house', + }, + ], + polymorphicOn: 'property_type', + }, + }, + ], + }, + ... + ], +``` + +When defined like this, adminforth will use value in `property_type` to figure out to what table does id in `property_id` refers to and properly link them. When creating or editing a record, adminforth will figure out to what table new `property_id` links to and fill `property_type` on its own using corresponding `whenValue`. Note, that `whenValue` does not have to be the same as `resourceId`, it can be any string as long as they do not repeat withing `polymorphicResources` array. Also, since `whenValue` is a string, column designated as `polymorphicOn` must also be string. Another thing to note is that, `polymorphicOn` column (`property_type` in our case) must not be editable by user, so it must include both `create` and `edit` as `false` in `showIn` value. Even though, `polymorphicOn` column is no editable, it can be beneficial to set is as an enumerator. This will have two benefits: first, columns value displayed in table and show page can be changed to a desired one and second, when filtering on this column, user will only able to choose values provided for him. + +If `beforeDatasourceRequest` or `afterDatasourceResponse` hooks are set for polymorphic foreign resource, they will be called for each resource in `polymorphicResources` array. + ## Filtering ### Filter Options diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 43a7c8e3f..367ac857a 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -507,14 +507,69 @@ export default class ConfigValidator implements IConfigValidator { if (col.foreignResource) { if (!col.foreignResource.resourceId) { - errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`); - } - // we do || here because 'resourceId' might yet not be assigned from 'table' - const resource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId); - if (!resource) { - const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), col.foreignResource.resourceId); - errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}". + // resourceId is absent or empty + if (!col.foreignResource.polymorphicResources && !col.foreignResource.polymorphicOn) { + // foreignResource is present but no specifying fields + errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`); + } else if (!col.foreignResource.polymorphicResources || !col.foreignResource.polymorphicOn) { + // some polymorphic fields are present but not all + if (!col.foreignResource.polymorphicResources) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphic foreign resource requires polymorphicResources field`); + } else { + errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphic foreign resource requires polymorphicOn field`); + } + } else { + // correct polymorphic structure + if (!col.foreignResource.polymorphicResources.length) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResources `); + } + // we do || here because 'resourceId' might yet not be assigned from 'table' + col.foreignResource.polymorphicResources.forEach((polymorphicResource, polymorphicResourceIndex) => { + if (!polymorphicResource.resourceId) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" has polymorphic foreign resource without resourceId`); + } else if (!polymorphicResource.whenValue) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" has polymorphic foreign resource without whenValue`); + } else { + const resource = this.inputConfig.resources.find((r) => r.resourceId === polymorphicResource.resourceId || r.table === polymorphicResource.resourceId); + if (!resource) { + const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), polymorphicResource.resourceId); + errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource polymorphicResource resourceId which is not in resources: "${polymorphicResource.resourceId}". + ${similar ? `Did you mean "${similar}" instead of "${polymorphicResource.resourceId}"?` : ''}`); + } + if (col.foreignResource.polymorphicResources.findIndex((pr) => pr.resourceId === polymorphicResource.resourceId) !== polymorphicResourceIndex) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResource resourceId should be unique`); + } + } + + if (col.foreignResource.polymorphicResources.findIndex((pr) => pr.whenValue === polymorphicResource.whenValue) !== polymorphicResourceIndex) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResource whenValue should be unique`); + } + }); + + const polymorphicOnInCol = resInput.columns.find((c) => c.name === col.foreignResource.polymorphicOn); + if (!polymorphicOnInCol) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn links to an unknown column`); + } else if (polymorphicOnInCol.type && polymorphicOnInCol.type !== AdminForthDataTypes.STRING) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn links to an column that is not of type string`); + } else { + const polymorphicOnColShowIn = this.validateAndNormalizeShowIn(resInput, polymorphicOnInCol, errors, warnings); + if (polymorphicOnColShowIn.create || polymorphicOnColShowIn.edit) { + errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn column should not be changeable manually`); + } + } + } + } else if (col.foreignResource.polymorphicResources || col.foreignResource.polymorphicOn) { + // both resourceId and polymorphic fields + errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource cannot have resourceId and be polymorphic at the same time`); + } else { + // non empty resourceId and no polymorphic fields + // we do || here because 'resourceId' might yet not be assigned from 'table' + const resource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId); + if (!resource) { + const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), col.foreignResource.resourceId); + errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}". ${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`); + } } if (col.foreignResource.unsetLabel) { diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 169e59d02..2f98b73d7 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -667,33 +667,84 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { // for foreign keys, add references await Promise.all( resource.columns.filter((col) => col.foreignResource).map(async (col) => { - const targetResource = this.adminforth.config.resources.find((res) => res.resourceId == col.foreignResource.resourceId); - const targetConnector = this.adminforth.connectors[targetResource.dataSource]; - const targetResourcePkField = targetResource.columns.find((col) => col.primaryKey).name; - const pksUnique = [...new Set(data.data.map((item) => item[col.name]))]; - if (pksUnique.length === 0) { - return; - } - const targetData = await targetConnector.getData({ - resource: targetResource, - limit: limit, - offset: 0, - filters: [ - { - field: targetResourcePkField, - operator: AdminForthFilterOperators.IN, - value: pksUnique, - } - ], - sort: [], - }); - const targetDataMap = targetData.data.reduce((acc, item) => { - acc[item[targetResourcePkField]] = { - label: targetResource.recordLabel(item), - pk: item[targetResourcePkField], + let targetDataMap = {}; + + if (col.foreignResource.resourceId) { + const targetResource = this.adminforth.config.resources.find((res) => res.resourceId == col.foreignResource.resourceId); + const targetConnector = this.adminforth.connectors[targetResource.dataSource]; + const targetResourcePkField = targetResource.columns.find((col) => col.primaryKey).name; + const pksUnique = [...new Set(data.data.map((item) => item[col.name]))]; + if (pksUnique.length === 0) { + return; } - return acc; - }, {}); + const targetData = await targetConnector.getData({ + resource: targetResource, + limit: limit, + offset: 0, + filters: [ + { + field: targetResourcePkField, + operator: AdminForthFilterOperators.IN, + value: pksUnique, + } + ], + sort: [], + }); + targetDataMap = targetData.data.reduce((acc, item) => { + acc[item[targetResourcePkField]] = { + label: targetResource.recordLabel(item), + pk: item[targetResourcePkField], + } + return acc; + }, {}); + } else { + const targetResources = {}; + const targetConnectors = {}; + const targetResourcePkFields = {}; + const pksUniques = {}; + col.foreignResource.polymorphicResources.forEach((pr) => { + targetResources[pr.whenValue] = this.adminforth.config.resources.find((res) => res.resourceId == pr.resourceId); + targetConnectors[pr.whenValue] = this.adminforth.connectors[targetResources[pr.whenValue].dataSource]; + targetResourcePkFields[pr.whenValue] = targetResources[pr.whenValue].columns.find((col) => col.primaryKey).name; + const pksUnique = [...new Set(data.data.filter((item) => item[col.foreignResource.polymorphicOn] === pr.whenValue).map((item) => item[col.name]))]; + if (pksUnique.length !== 0) { + pksUniques[pr.whenValue] = pksUnique; + } + if (Object.keys(pksUniques).length === 0) { + return; + } + }); + + const targetData = (await Promise.all(Object.keys(pksUniques).map((polymorphicOnValue) => + targetConnectors[polymorphicOnValue].getData({ + resource: targetResources[polymorphicOnValue], + limit: limit, + offset: 0, + filters: [ + { + field: targetResourcePkFields[polymorphicOnValue], + operator: AdminForthFilterOperators.IN, + value: pksUniques[polymorphicOnValue], + } + ], + sort: [], + }) + ))).reduce((acc: any, td: any, tdi) => ({ + ...acc, + [Object.keys(pksUniques)[tdi]]: td, + }), {}); + targetDataMap = Object.keys(targetData).reduce((tdAcc, polymorphicOnValue) => ({ + ...tdAcc, + ...targetData[polymorphicOnValue].data.reduce((dAcc, item) => { + dAcc[item[targetResourcePkFields[polymorphicOnValue]]] = { + label: targetResources[polymorphicOnValue].recordLabel(item), + pk: item[targetResourcePkFields[polymorphicOnValue]], + } + return dAcc; + }, {}), + }), {}); + } + data.data.forEach((item) => { item[col.name] = targetDataMap[item[col.name]]; }); @@ -767,74 +818,85 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { if (!columnConfig.foreignResource) { return { error: `Column '${column}' in resource '${resourceId}' is not a foreign key` }; } - const targetResourceId = columnConfig.foreignResource.resourceId; - const targetResource = this.adminforth.config.resources.find((res) => res.resourceId == targetResourceId); - - for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest as BeforeDataSourceRequestFunction[])) { - const resp = await hook({ - query: body, - adminUser, - resource: targetResource, - extra: { - body, query, headers, cookies, requestUrl - }, - adminforth: this.adminforth, - }); - if (!resp || (!resp.ok && !resp.error)) { - throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `); - } - if (resp.error) { - return { error: resp.error }; - } - } - const { limit, offset, filters, sort } = body; - const dbDataItems = await this.adminforth.connectors[targetResource.dataSource].getData({ - resource: targetResource, - limit, - offset, - filters: filters || [], - sort: sort || [], - }); - const items = dbDataItems.data.map((item) => { - const pk = item[targetResource.columns.find((col) => col.primaryKey).name]; - const labler = targetResource.recordLabel; - return { - value: pk, - label: labler(item), - _item: item, // user might need it in hook to form new label - } - }); - const response = { - items - }; + const targetResourceIds = columnConfig.foreignResource.resourceId ? [columnConfig.foreignResource.resourceId] : columnConfig.foreignResource.polymorphicResources.map((pr) => pr.resourceId); + const targetResources = targetResourceIds.map((trId) => this.adminforth.config.resources.find((res) => res.resourceId == trId)); + + const responses = (await Promise.all( + targetResources.map(async (targetResource) => { + return new Promise(async (resolve) => { + for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest as BeforeDataSourceRequestFunction[])) { + const resp = await hook({ + query: body, + adminUser, + resource: targetResource, + extra: { + body, query, headers, cookies, requestUrl + }, + adminforth: this.adminforth, + }); + if (!resp || (!resp.ok && !resp.error)) { + throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `); + } - for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.afterDatasourceResponse as AfterDataSourceResponseFunction[])) { - const resp = await hook({ - response, - adminUser, - query: body, - resource: targetResource, - extra: { - body, query, headers, cookies, requestUrl - }, - adminforth: this.adminforth, - }); - if (!resp || (!resp.ok && !resp.error)) { - throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `); - } + if (resp.error) { + return { error: resp.error }; + } + } + const { limit, offset, filters, sort } = body; + const dbDataItems = await this.adminforth.connectors[targetResource.dataSource].getData({ + resource: targetResource, + limit, + offset, + filters: filters || [], + sort: sort || [], + }); + const items = dbDataItems.data.map((item) => { + const pk = item[targetResource.columns.find((col) => col.primaryKey).name]; + const labler = targetResource.recordLabel; + return { + value: pk, + label: labler(item), + _item: item, // user might need it in hook to form new label + } + }); + const response = { + items + }; + + for (const hook of listify(columnConfig.foreignResource.hooks?.dropdownList?.afterDatasourceResponse as AfterDataSourceResponseFunction[])) { + const resp = await hook({ + response, + adminUser, + query: body, + resource: targetResource, + extra: { + body, query, headers, cookies, requestUrl + }, + adminforth: this.adminforth, + }); + if (!resp || (!resp.ok && !resp.error)) { + throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `); + } - if (resp.error) { - return { error: resp.error }; - } - } + if (resp.error) { + return { error: resp.error }; + } + } - // remove _item from response (might expose sensitive data like backendOnly fields) - response.items.forEach((item) => { - delete item._item; - }); - - return response; + // remove _item from response (might expose sensitive data like backendOnly fields) + response.items.forEach((item) => { + delete item._item; + }); + + resolve(response); + }); + }) + )).reduce((acc: any, response: any) => { + return [...acc, ...response.items]; + }, []); + + return { items: responses }; }, }); @@ -904,6 +966,40 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { } } } + + // for polymorphic foreign resources, we need to find out the value for polymorphicOn column + for (const column of resource.columns) { + if (column.foreignResource?.polymorphicOn && record[column.name]) { + const targetResources = {}; + const targetConnectors = {}; + const targetResourcePkFields = {}; + column.foreignResource.polymorphicResources.forEach((pr) => { + targetResources[pr.whenValue] = this.adminforth.config.resources.find((res) => res.resourceId == pr.resourceId); + targetConnectors[pr.whenValue] = this.adminforth.connectors[targetResources[pr.whenValue].dataSource]; + targetResourcePkFields[pr.whenValue] = targetResources[pr.whenValue].columns.find((col) => col.primaryKey).name; + }); + + const targetData = (await Promise.all(Object.keys(targetResources).map((polymorphicOnValue) => + targetConnectors[polymorphicOnValue].getData({ + resource: targetResources[polymorphicOnValue], + limit: 1, + offset: 0, + filters: [ + { + field: targetResourcePkFields[polymorphicOnValue], + operator: AdminForthFilterOperators.EQ, + value: record[column.name], + } + ], + sort: [], + }) + ))).reduce((acc: any, td: any, tdi) => ({ + ...acc, + [Object.keys(targetResources)[tdi]]: td.data, + }), {}); + record[column.foreignResource.polymorphicOn] = Object.keys(targetData).find((tdk) => targetData[tdk].length); + } + } const response = await this.adminforth.createResourceRecord({ resource, record, adminUser, extra: { body, query, headers, cookies, requestUrl } }); if (response.error) { @@ -957,6 +1053,47 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { } } + // for polymorphic foreign resources, we need to find out the value for polymorphicOn column + for (const column of resource.columns) { + if (column.foreignResource?.polymorphicOn) { + let newPolymorphicOnValue = null; + if (record[column.name]) { + const targetResources = {}; + const targetConnectors = {}; + const targetResourcePkFields = {}; + column.foreignResource.polymorphicResources.forEach((pr) => { + targetResources[pr.whenValue] = this.adminforth.config.resources.find((res) => res.resourceId == pr.resourceId); + targetConnectors[pr.whenValue] = this.adminforth.connectors[targetResources[pr.whenValue].dataSource]; + targetResourcePkFields[pr.whenValue] = targetResources[pr.whenValue].columns.find((col) => col.primaryKey).name; + }); + + const targetData = (await Promise.all(Object.keys(targetResources).map((polymorphicOnValue) => + targetConnectors[polymorphicOnValue].getData({ + resource: targetResources[polymorphicOnValue], + limit: 1, + offset: 0, + filters: [ + { + field: targetResourcePkFields[polymorphicOnValue], + operator: AdminForthFilterOperators.EQ, + value: record[column.name], + } + ], + sort: [], + }) + ))).reduce((acc: any, td: any, tdi) => ({ + ...acc, + [Object.keys(targetResources)[tdi]]: td.data, + }), {}); + newPolymorphicOnValue = Object.keys(targetData).find((tdk) => targetData[tdk].length); + } + + if (oldRecord[column.foreignResource.polymorphicOn] !== newPolymorphicOnValue) { + record[column.foreignResource.polymorphicOn] = newPolymorphicOnValue; + } + } + } + const { error } = await this.adminforth.updateResourceRecord({ resource, record, adminUser, oldRecord, recordId, extra: { body, query, headers, cookies, requestUrl} }); if (error) { return { error }; diff --git a/adminforth/spa/src/components/ResourceListTable.vue b/adminforth/spa/src/components/ResourceListTable.vue index d4bae6ef6..284174ed0 100644 --- a/adminforth/spa/src/components/ResourceListTable.vue +++ b/adminforth/spa/src/components/ResourceListTable.vue @@ -366,7 +366,7 @@ const rowRefs = useTemplateRef('rowRefs'); const rowHeights = ref([]); watch(() => props.rows, (newRows) => { // rows are set to null when new records are loading - rowHeights.value = newRows ? [] : rowRefs.value.map((el) => el.offsetHeight); + rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el) => el.offsetHeight); }); function addToCheckedValues(id) { diff --git a/adminforth/spa/src/components/ValueRenderer.vue b/adminforth/spa/src/components/ValueRenderer.vue index 90b780c85..b996f5d73 100644 --- a/adminforth/spa/src/components/ValueRenderer.vue +++ b/adminforth/spa/src/components/ValueRenderer.vue @@ -2,7 +2,7 @@
+ :to="{ name: 'resource-show', params: { primaryKey: record[column.name].pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }"> {{ record[column.name].label }}
diff --git a/adminforth/types/Common.ts b/adminforth/types/Common.ts index 56429139d..b3fe73c0e 100644 --- a/adminforth/types/Common.ts +++ b/adminforth/types/Common.ts @@ -540,9 +540,14 @@ export type ShowInResolved = { [key in AdminForthResourcePages]: boolean } - -export interface AdminForthForeignResourceCommon { +export interface AdminForthPolymorphicForeignResource { resourceId: string, + whenValue: string, +} +export interface AdminForthForeignResourceCommon { + resourceId?: string, + polymorphicResources?: Array, + polymorphicOn?: string, unsetLabel?: string, } diff --git a/dev-demo/index.ts b/dev-demo/index.ts index 978729e16..35b8e5a28 100644 --- a/dev-demo/index.ts +++ b/dev-demo/index.ts @@ -13,6 +13,9 @@ import usersResource from './resources/users.js'; // import gamesUsersResource from './resources/games_users.js'; // import gamesResource from './resources/games.js'; import translationsResource from './resources/translation.js'; +import clinicsResource from './resources/clinics.js'; +import providersResource from './resources/providers.js'; +import apiKeysResource from './resources/api_keys.js'; import CompletionAdapterOpenAIChatGPT from '../adapters/adminforth-completion-adapter-open-ai-chat-gpt/index.js'; // const ADMIN_BASE_URL = '/portal'; @@ -203,6 +206,9 @@ export const admin = new AdminForth({ apartmentBuyersResource, usersResource, descriptionImageResource, + clinicsResource, + providersResource, + apiKeysResource, // gamesResource, // gamesUsersResource, // gameResource, @@ -264,6 +270,21 @@ export const admin = new AdminForth({ // resourceId: 'game', // }, + { + label: 'Clinics', + icon: 'flowbite:building-solid', + resourceId: 'clinics', + }, + { + label: 'Providers', + icon: 'flowbite:user-solid', + resourceId: 'providers', + }, + { + label: 'API Keys', + icon: 'flowbite:search-outline', + resourceId: 'api_keys', + }, { label: 'Clicks', icon: 'flowbite:search-outline', diff --git a/dev-demo/migrations/20250227084330_polyamorphic_tables_init/migration.sql b/dev-demo/migrations/20250227084330_polyamorphic_tables_init/migration.sql new file mode 100644 index 000000000..ae01e5a66 --- /dev/null +++ b/dev-demo/migrations/20250227084330_polyamorphic_tables_init/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "clinics" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "providers" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "api_keys" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "owner" TEXT, + "owner_id" TEXT +); diff --git a/dev-demo/resources/api_keys.ts b/dev-demo/resources/api_keys.ts new file mode 100644 index 000000000..cd055ae8b --- /dev/null +++ b/dev-demo/resources/api_keys.ts @@ -0,0 +1,64 @@ +import { AdminForthDataTypes, AdminForthResourceInput } from "../../adminforth"; +import { v1 as uuid } from "uuid"; + +export default { + dataSource: 'maindb', + table: 'api_keys', + resourceId: 'api_keys', + label: 'API Keys', + recordLabel: (r: any) => `🔑 ${r.name}`, + columns: [ + { + name: 'id', + label: 'ID', + primaryKey: true, + fillOnCreate: ({ initialRecord, adminUser }: any) => uuid(), + showIn: { + create: false, + edit: false, + }, + components: { + list: "@/renderers/CompactUUID.vue", + }, + }, + { + name: 'name', + type: AdminForthDataTypes.STRING, + required: true, + maxLength: 255, + }, + { + name: 'owner', + type: AdminForthDataTypes.STRING, + enum: [ + { + value: 'clinic', + label: 'Clinic', + }, + { + value: 'provider', + label: 'Provider', + }, + ], + showIn: { create: false, edit: false }, + }, + { + name: 'owner_id', + foreignResource: { + polymorphicResources: [ + { + resourceId: 'clinics', + whenValue: 'clinic', + }, + { + resourceId: 'providers', + whenValue: 'provider', + }, + ], + polymorphicOn: 'owner', + }, + }, + ], + plugins: [], + options: {}, +} as AdminForthResourceInput; \ No newline at end of file diff --git a/dev-demo/resources/clinics.ts b/dev-demo/resources/clinics.ts new file mode 100644 index 000000000..0e32296f9 --- /dev/null +++ b/dev-demo/resources/clinics.ts @@ -0,0 +1,33 @@ +import { AdminForthDataTypes, AdminForthResourceInput } from "../../adminforth"; +import { v1 as uuid } from "uuid"; + +export default { + dataSource: 'maindb', + table: 'clinics', + resourceId: 'clinics', + label: 'Clinics', + recordLabel: (r: any) => `🏥 ${r.name}`, + columns: [ + { + name: 'id', + label: 'ID', + primaryKey: true, + fillOnCreate: ({ initialRecord, adminUser }: any) => uuid(), + showIn: { + create: false, + edit: false, + }, + components: { + list: "@/renderers/CompactUUID.vue", + }, + }, + { + name: 'name', + type: AdminForthDataTypes.STRING, + required: true, + maxLength: 255, + }, + ], + plugins: [], + options: {}, +} as AdminForthResourceInput; \ No newline at end of file diff --git a/dev-demo/resources/providers.ts b/dev-demo/resources/providers.ts new file mode 100644 index 000000000..ed3ffc6d0 --- /dev/null +++ b/dev-demo/resources/providers.ts @@ -0,0 +1,33 @@ +import { AdminForthDataTypes, AdminForthResourceInput } from "../../adminforth"; +import { v1 as uuid } from "uuid"; + +export default { + dataSource: 'maindb', + table: 'providers', + resourceId: 'providers', + label: 'Providers', + recordLabel: (r: any) => `👨‍⚕️ ${r.name}`, + columns: [ + { + name: 'id', + label: 'ID', + primaryKey: true, + fillOnCreate: ({ initialRecord, adminUser }: any) => uuid(), + showIn: { + create: false, + edit: false, + }, + components: { + list: "@/renderers/CompactUUID.vue", + }, + }, + { + name: 'name', + type: AdminForthDataTypes.STRING, + required: true, + maxLength: 255, + }, + ], + plugins: [], + options: {}, +} as AdminForthResourceInput; \ No newline at end of file diff --git a/dev-demo/schema.prisma b/dev-demo/schema.prisma index 2bcd0ca0e..3d41cd3ac 100644 --- a/dev-demo/schema.prisma +++ b/dev-demo/schema.prisma @@ -104,4 +104,21 @@ model apartment_buyers { contact_date DateTime? contact_time DateTime? realtor_id String? +} + +model clinics { + id String @id + name String +} + +model providers { + id String @id + name String +} + +model api_keys { + id String @id + name String + owner String? + owner_id String? } \ No newline at end of file