mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
Merge pull request #4725 from logto-io/gao-console-org-12
refactor(core,console): fixing to-dos
This commit is contained in:
commit
5b2e4c87b1
29 changed files with 153 additions and 74 deletions
|
@ -5,6 +5,10 @@
|
|||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
> svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> div:not(:first-child) {
|
||||
margin-left: _.unit(3);
|
||||
}
|
||||
|
|
|
@ -47,12 +47,15 @@ function AddMembersToOrganization({ organization, isOpen, onClose }: Props) {
|
|||
userIds: data.users.map(({ id }) => id),
|
||||
},
|
||||
});
|
||||
await api.post(`api/organizations/${organization.id}/users/roles`, {
|
||||
json: {
|
||||
userIds: data.users.map(({ id }) => id),
|
||||
roleIds: data.scopes.map(({ value }) => value),
|
||||
},
|
||||
});
|
||||
|
||||
if (data.scopes.length > 0) {
|
||||
await api.post(`api/organizations/${organization.id}/users/roles`, {
|
||||
json: {
|
||||
userIds: data.users.map(({ id }) => id),
|
||||
roleIds: data.scopes.map(({ value }) => value),
|
||||
},
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
|
@ -7,6 +7,7 @@ import Plus from '@/assets/icons/plus.svg';
|
|||
import ActionsButton from '@/components/ActionsButton';
|
||||
import DateTime from '@/components/DateTime';
|
||||
import UserPreview from '@/components/ItemPreview/UserPreview';
|
||||
import { defaultPageSize } from '@/consts';
|
||||
import Button from '@/ds-components/Button';
|
||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||
import Search from '@/ds-components/Search';
|
||||
|
@ -20,6 +21,8 @@ import AddMembersToOrganization from './AddMembersToOrganization';
|
|||
import EditOrganizationRolesModal from './EditOrganizationRolesModal';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const pageSize = defaultPageSize;
|
||||
|
||||
type Props = {
|
||||
organization: Organization;
|
||||
};
|
||||
|
@ -27,8 +30,17 @@ type Props = {
|
|||
function Members({ organization }: Props) {
|
||||
const api = useApi();
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const { data, error, mutate } = useSWR<UserWithOrganizationRoles[], RequestError>(
|
||||
buildUrl(`api/organizations/${organization.id}/users`, { q: keyword })
|
||||
const [page, setPage] = useState(1);
|
||||
const {
|
||||
data: response,
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR<[UserWithOrganizationRoles[], number], RequestError>(
|
||||
buildUrl(`api/organizations/${organization.id}/users`, {
|
||||
q: keyword,
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
})
|
||||
);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const tAction = useActionTranslation();
|
||||
|
@ -39,13 +51,21 @@ function Members({ organization }: Props) {
|
|||
return null; // TODO: error handling
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
if (!response) {
|
||||
return null; // TODO: loading
|
||||
}
|
||||
|
||||
const [data, totalCount] = response;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
pagination={{
|
||||
page,
|
||||
totalCount,
|
||||
pageSize,
|
||||
onChange: setPage,
|
||||
}}
|
||||
rowGroups={[{ key: 'data', data }]}
|
||||
columns={[
|
||||
{
|
||||
|
@ -113,9 +133,11 @@ function Members({ organization }: Props) {
|
|||
placeholder={t('organization_details.search_user_placeholder')}
|
||||
onSearch={(value) => {
|
||||
setKeyword(value);
|
||||
setPage(1);
|
||||
}}
|
||||
onClearSearch={() => {
|
||||
setKeyword('');
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
padding-bottom: _.unit(6);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -61,7 +61,7 @@ function OrganizationDetails() {
|
|||
}
|
||||
|
||||
return (
|
||||
<DetailsPage backLink={pathname} backLinkTitle="organizations.title">
|
||||
<DetailsPage backLink={pathname} backLinkTitle="organizations.title" className={styles.page}>
|
||||
<PageMeta titleKey="organization_details.page_title" />
|
||||
<Card className={styles.header}>
|
||||
<div className={styles.metadata}>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { type GeneratedSchema, type SchemaLike } from '@logto/schemas';
|
|||
import { conditionalSql, convertToIdentifiers, manyRows } from '@logto/shared';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
|
||||
import { buildSearchSql, type SearchOptions } from './utils.js';
|
||||
import { buildSearchSql, expandFields, type SearchOptions } from './utils.js';
|
||||
|
||||
export const buildFindAllEntitiesWithPool =
|
||||
(pool: CommonQueryMethods) =>
|
||||
|
@ -26,7 +26,7 @@ export const buildFindAllEntitiesWithPool =
|
|||
) =>
|
||||
manyRows(
|
||||
pool.query<Schema>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
select ${expandFields(schema)}
|
||||
from ${table}
|
||||
${buildSearchSql(schema, search)}
|
||||
${conditionalSql(orderBy, (orderBy) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type GeneratedSchema } from '@logto/schemas';
|
||||
import { type SchemaLike, conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import { type SchemaLike, conditionalSql, convertToIdentifiers, type Table } from '@logto/shared';
|
||||
import { type SqlSqlToken, sql } from 'slonik';
|
||||
|
||||
/**
|
||||
|
@ -44,3 +44,15 @@ export const buildSearchSql = <
|
|||
return sql`${prefixSql}(${searchSql})`;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand the fields of a schema into a SQL list. Useful for `select` statements.
|
||||
*
|
||||
* @param schema The schema to expand.
|
||||
* @param tablePrefix Whether to prefix the fields with the table name.
|
||||
* @returns The generated SQL list separated by `, `.
|
||||
*/
|
||||
export const expandFields = <Keys extends string>(schema: Table<Keys>, tablePrefix = false) => {
|
||||
const { fields } = convertToIdentifiers(schema, tablePrefix);
|
||||
return sql.join(Object.values(fields), sql`, `);
|
||||
};
|
||||
|
|
|
@ -19,8 +19,11 @@ import {
|
|||
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
|
||||
import { type SearchOptions, buildSearchSql } from '#src/database/utils.js';
|
||||
import RelationQueries, { TwoRelationsQueries } from '#src/utils/RelationQueries.js';
|
||||
import { type SearchOptions, buildSearchSql, expandFields } from '#src/database/utils.js';
|
||||
import RelationQueries, {
|
||||
type GetEntitiesOptions,
|
||||
TwoRelationsQueries,
|
||||
} from '#src/utils/RelationQueries.js';
|
||||
import SchemaQueries from '#src/utils/SchemaQueries.js';
|
||||
|
||||
import { type userSearchKeys } from './user.js';
|
||||
|
@ -37,10 +40,9 @@ class UserRelationQueries extends TwoRelationsQueries<typeof Organizations, type
|
|||
const { fields } = convertToIdentifiers(OrganizationUserRelations, true);
|
||||
const relations = convertToIdentifiers(OrganizationRoleUserRelations, true);
|
||||
|
||||
// TODO: replace `.*` with explicit fields
|
||||
return this.pool.any<OrganizationWithRoles>(sql`
|
||||
select
|
||||
${organizations.table}.*,
|
||||
${expandFields(Organizations, true)},
|
||||
${this.#aggregateRoles()}
|
||||
from ${this.table}
|
||||
left join ${organizations.table}
|
||||
|
@ -58,29 +60,44 @@ class UserRelationQueries extends TwoRelationsQueries<typeof Organizations, type
|
|||
/** Get the users in an organization and their roles. */
|
||||
async getUsersByOrganizationId(
|
||||
organizationId: string,
|
||||
{ limit, offset }: GetEntitiesOptions,
|
||||
search?: SearchOptions<(typeof userSearchKeys)[number]>
|
||||
): Promise<Readonly<UserWithOrganizationRoles[]>> {
|
||||
): Promise<[totalCount: number, entities: Readonly<UserWithOrganizationRoles[]>]> {
|
||||
const roles = convertToIdentifiers(OrganizationRoles, true);
|
||||
const users = convertToIdentifiers(Users, true);
|
||||
const { fields } = convertToIdentifiers(OrganizationUserRelations, true);
|
||||
const relations = convertToIdentifiers(OrganizationRoleUserRelations, true);
|
||||
|
||||
return this.pool.any<UserWithOrganizationRoles>(sql`
|
||||
select
|
||||
${users.table}.*,
|
||||
${this.#aggregateRoles()}
|
||||
from ${this.table}
|
||||
left join ${users.table}
|
||||
on ${fields.userId} = ${users.fields.id}
|
||||
left join ${relations.table}
|
||||
on ${fields.userId} = ${relations.fields.userId}
|
||||
and ${fields.organizationId} = ${relations.fields.organizationId}
|
||||
left join ${roles.table}
|
||||
on ${relations.fields.organizationRoleId} = ${roles.fields.id}
|
||||
where ${fields.organizationId} = ${organizationId}
|
||||
${buildSearchSql(Users, search, sql`and `)}
|
||||
group by ${users.table}.id
|
||||
`);
|
||||
const [{ count }, entities] = await Promise.all([
|
||||
this.pool.one<{ count: string }>(sql`
|
||||
select count(*)
|
||||
from ${this.table}
|
||||
left join ${users.table}
|
||||
on ${fields.userId} = ${users.fields.id}
|
||||
where ${fields.organizationId} = ${organizationId}
|
||||
${buildSearchSql(Users, search, sql`and `)}
|
||||
`),
|
||||
this.pool.any<UserWithOrganizationRoles>(sql`
|
||||
select
|
||||
${users.table}.*,
|
||||
${this.#aggregateRoles()}
|
||||
from ${this.table}
|
||||
left join ${users.table}
|
||||
on ${fields.userId} = ${users.fields.id}
|
||||
left join ${relations.table}
|
||||
on ${fields.userId} = ${relations.fields.userId}
|
||||
and ${fields.organizationId} = ${relations.fields.organizationId}
|
||||
left join ${roles.table}
|
||||
on ${relations.fields.organizationRoleId} = ${roles.fields.id}
|
||||
where ${fields.organizationId} = ${organizationId}
|
||||
${buildSearchSql(Users, search, sql`and `)}
|
||||
group by ${users.table}.id
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`),
|
||||
]);
|
||||
|
||||
return [Number(count), entities];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,7 +32,7 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
|
||||
router.get(
|
||||
'/:id/users',
|
||||
// TODO: support pagination
|
||||
koaPagination(),
|
||||
koaGuard({
|
||||
query: z.object({ q: z.string().optional() }),
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
|
@ -47,10 +47,16 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
keyword: q,
|
||||
}
|
||||
);
|
||||
ctx.body = await organizations.relations.users.getUsersByOrganizationId(
|
||||
|
||||
const [totalCount, entities] = await organizations.relations.users.getUsersByOrganizationId(
|
||||
ctx.guard.params.id,
|
||||
ctx.pagination,
|
||||
search
|
||||
);
|
||||
|
||||
ctx.pagination.totalCount = totalCount;
|
||||
ctx.body = entities;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
@ -85,6 +91,9 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
const params = Object.freeze({ id: z.string().min(1), userId: z.string().min(1) } as const);
|
||||
const pathname = '/:id/users/:userId/roles';
|
||||
|
||||
// The pathname of `.use()` will not match the end of the path, for example:
|
||||
// `.use('/foo', ...)` will match both `/foo` and `/foo/bar`.
|
||||
// See https://github.com/koajs/router/blob/02ad6eedf5ced6ec1eab2138380fd67c63e3f1d7/lib/router.js#L330-L333
|
||||
router.use(pathname, koaGuard({ params: z.object(params) }), async (ctx, next) => {
|
||||
const { id, userId } = ctx.guard.params;
|
||||
|
||||
|
@ -160,7 +169,6 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
}
|
||||
);
|
||||
|
||||
// TODO: check if membership is required in this route
|
||||
router.delete(
|
||||
`${pathname}/:roleId`,
|
||||
koaGuard({
|
||||
|
|
|
@ -7,7 +7,7 @@ import RequestError from '#src/errors/RequestError/index.js';
|
|||
|
||||
export const errorHandler = (error: unknown) => {
|
||||
if (error instanceof UniqueIntegrityConstraintViolationError) {
|
||||
throw new RequestError({ code: 'entity.duplicate_value_of_unique_field', field: 'name' }); // TODO: specify field
|
||||
throw new RequestError({ code: 'entity.unique_integrity_violation', status: 422 });
|
||||
}
|
||||
|
||||
if (error instanceof ForeignKeyIntegrityConstraintViolationError) {
|
||||
|
|
|
@ -1,28 +1,34 @@
|
|||
import { conditionalSql } from '@logto/shared';
|
||||
import { type Table, conditionalSql } from '@logto/shared';
|
||||
import { type KeysToCamelCase } from '@silverhand/essentials';
|
||||
import { sql, type CommonQueryMethods } from 'slonik';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
import { type z } from 'zod';
|
||||
|
||||
import { expandFields } from '#src/database/utils.js';
|
||||
import { DeletionError } from '#src/errors/SlonikError/index.js';
|
||||
|
||||
type AtLeast2<T extends unknown[]> = `${T['length']}` extends '0' | '1' ? never : T;
|
||||
|
||||
type TableInfo<Table, TableSingular, Schema> = {
|
||||
table: Table;
|
||||
type TableInfo<
|
||||
TableName extends string,
|
||||
TableSingular extends string,
|
||||
Key extends string,
|
||||
Schema,
|
||||
> = Table<Key, TableName> & {
|
||||
tableSingular: TableSingular;
|
||||
guard: z.ZodType<Schema, z.ZodTypeDef, unknown>;
|
||||
};
|
||||
|
||||
type InferSchema<T> = T extends TableInfo<infer _, infer _, infer Schema> ? Schema : never;
|
||||
type InferSchema<T> = T extends TableInfo<infer _, infer _, infer _, infer Schema> ? Schema : never;
|
||||
|
||||
type CamelCaseIdObject<T extends string> = KeysToCamelCase<{
|
||||
[Key in `${T}_id`]: string;
|
||||
}>;
|
||||
|
||||
type GetEntitiesOptions = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
/** Options for getting entities in a table. */
|
||||
export type GetEntitiesOptions = {
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -59,7 +65,7 @@ type GetEntitiesOptions = {
|
|||
* group with the id `group-id-1`.
|
||||
*/
|
||||
export default class RelationQueries<
|
||||
Schemas extends Array<TableInfo<string, string, unknown>>,
|
||||
Schemas extends Array<TableInfo<string, string, string, unknown>>,
|
||||
Length = AtLeast2<Schemas>['length'],
|
||||
> {
|
||||
protected get table() {
|
||||
|
@ -175,9 +181,9 @@ export default class RelationQueries<
|
|||
async getEntities<S extends Schemas[number]>(
|
||||
forSchema: S,
|
||||
where: CamelCaseIdObject<Exclude<Schemas[number]['tableSingular'], S['tableSingular']>>,
|
||||
options: GetEntitiesOptions = {}
|
||||
options?: GetEntitiesOptions
|
||||
): Promise<[totalCount: number, entities: ReadonlyArray<InferSchema<S>>]> {
|
||||
const { limit, offset } = options;
|
||||
const { limit, offset } = options ?? {};
|
||||
const snakeCaseWhere = snakecaseKeys(where);
|
||||
const forTable = sql.identifier([forSchema.table]);
|
||||
const mainSql = sql`
|
||||
|
@ -201,9 +207,10 @@ export default class RelationQueries<
|
|||
select count(*)
|
||||
${mainSql}
|
||||
`),
|
||||
// TODO: replace `.*` with explicit fields
|
||||
|
||||
this.pool.query<InferSchema<S>>(sql`
|
||||
select ${forTable}.* ${mainSql}
|
||||
select ${expandFields(forSchema, true)}
|
||||
${mainSql}
|
||||
${conditionalSql(limit, (limit) => sql`limit ${limit}`)}
|
||||
${conditionalSql(offset, (offset) => sql`offset ${offset}`)}
|
||||
`),
|
||||
|
@ -249,8 +256,8 @@ export default class RelationQueries<
|
|||
* @see {@link RelationQueries} for more information.
|
||||
*/
|
||||
export class TwoRelationsQueries<
|
||||
Schema1 extends TableInfo<string, string, unknown>,
|
||||
Schema2 extends TableInfo<string, string, unknown>,
|
||||
Schema1 extends TableInfo<string, string, string, unknown>,
|
||||
Schema2 extends TableInfo<string, string, string, unknown>,
|
||||
> extends RelationQueries<[Schema1, Schema2]> {
|
||||
/**
|
||||
* Replace all relations for a specific `Schema1` entity with the given `Schema2` entities.
|
||||
|
|
|
@ -21,10 +21,8 @@ describe('organization role APIs', () => {
|
|||
|
||||
const { statusCode, body: raw } = response.response;
|
||||
const body: unknown = JSON.parse(String(raw));
|
||||
expect(statusCode).toBe(400);
|
||||
expect(isKeyInObject(body, 'code') && body.code).toBe(
|
||||
'entity.duplicate_value_of_unique_field'
|
||||
);
|
||||
expect(statusCode).toBe(422);
|
||||
expect(isKeyInObject(body, 'code') && body.code).toBe('entity.unique_integrity_violation');
|
||||
|
||||
await roleApi.delete(createdRole.id);
|
||||
});
|
||||
|
|
|
@ -18,8 +18,8 @@ describe('organization scopes', () => {
|
|||
|
||||
const { statusCode, body: raw } = response.response;
|
||||
const body: unknown = JSON.parse(String(raw));
|
||||
expect(statusCode).toBe(400);
|
||||
expect(isKeyInObject(body, 'code') && body.code).toBe('entity.duplicate_value_of_unique_field');
|
||||
expect(statusCode).toBe(422);
|
||||
expect(isKeyInObject(body, 'code') && body.code).toBe('entity.unique_integrity_violation');
|
||||
});
|
||||
|
||||
it('should get organization scopes successfully', async () => {
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -7,7 +7,7 @@ const entity = {
|
|||
not_found: 'The resource does not exist.',
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -9,7 +9,7 @@ const entity = {
|
|||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
unique_integrity_violation: 'The entity already exists. Please check the input and try again.',
|
||||
};
|
||||
|
||||
export default Object.freeze(entity);
|
||||
|
|
|
@ -6,7 +6,10 @@ export type SchemaLike<Key extends string> = {
|
|||
[key in Key]: SchemaValue;
|
||||
};
|
||||
|
||||
export type Table<Keys extends string> = { table: string; fields: Record<Keys, string> };
|
||||
export type Table<Keys extends string, TableName extends string = string> = {
|
||||
table: TableName;
|
||||
fields: Record<Keys, string>;
|
||||
};
|
||||
export type FieldIdentifiers<Key extends string> = {
|
||||
[key in Key]: IdentifierSqlToken;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue