From 18a142ab658c577ad41b658ca28c17fc4220114c Mon Sep 17 00:00:00 2001 From: simeng-li Date: Wed, 1 Dec 2021 11:41:16 +0800 Subject: [PATCH] feat(schema): add resource and scope table (#145) * feat(schema): add resource and scope table implement resource-servers and resource-scopes table with some basic queries * fix(schema): remove scope update querie not allowed to update scope once created * feat(schema): add resourceId foreign key to the scope table add resourceId foreign key to the scope table * fix(schema): remove trailling comma remove trailling comma * fix(schema): cr fix replace resource-servers table name using resources replace resource-servers table name using resources --- packages/core/src/queries/resources.ts | 70 +++++++++++++++++++ packages/core/src/queries/scopes.ts | 35 ++++++++++ .../schemas/src/db-entries/custom-types.ts | 6 ++ packages/schemas/src/db-entries/index.ts | 2 + .../schemas/src/db-entries/resource-scope.ts | 32 +++++++++ packages/schemas/src/db-entries/resource.ts | 39 +++++++++++ packages/schemas/src/gen/index.ts | 12 +++- packages/schemas/tables/resource_scopes.sql | 16 +++++ packages/schemas/tables/resources.sql | 12 ++++ 9 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/queries/resources.ts create mode 100644 packages/core/src/queries/scopes.ts create mode 100644 packages/schemas/src/db-entries/resource-scope.ts create mode 100644 packages/schemas/src/db-entries/resource.ts create mode 100644 packages/schemas/tables/resource_scopes.sql create mode 100644 packages/schemas/tables/resources.sql diff --git a/packages/core/src/queries/resources.ts b/packages/core/src/queries/resources.ts new file mode 100644 index 000000000..d9f7d7d2d --- /dev/null +++ b/packages/core/src/queries/resources.ts @@ -0,0 +1,70 @@ +import { ResourceDBEntry, Resources } from '@logto/schemas'; +import { sql } from 'slonik'; + +import { buildInsertInto } from '@/database/insert-into'; +import pool from '@/database/pool'; +import { buildUpdateWhere } from '@/database/update-where'; +import { convertToIdentifiers, OmitAutoSetFields } from '@/database/utils'; +import RequestError from '@/errors/RequestError'; + +const { table, fields } = convertToIdentifiers(Resources); + +export const findAllResources = async () => + pool.many(sql` + select ${sql.join(Object.values(fields), sql`,`)} + from ${table} + `); + +export const hasResource = async (indentifier: string) => + pool.exists(sql` + select ${fields.id} + from ${table} + where ${fields.identifier}=${indentifier} +`); + +export const hasResourceWithId = async (id: string) => + pool.exists(sql` + select ${fields.id} + from ${table} + where ${fields.id}=${id} +`); + +export const findResourceByIdentifier = async (indentifier: string) => + pool.one(sql` + select ${sql.join(Object.values(fields), sql`,`)} + from ${table} + where ${fields.identifier}=${indentifier} +`); + +export const findResourceById = async (id: string) => + pool.one(sql` + select ${sql.join(Object.values(fields), sql`,`)} + from ${table} + where ${fields.id}=${id} +`); + +export const insertResource = buildInsertInto(pool, Resources, { + returning: true, +}); + +const updateResource = buildUpdateWhere(pool, Resources, true); + +export const updateResourceById = async ( + id: string, + set: Partial> +) => updateResource({ set, where: { id } }); + +export const deleteResourceById = async (id: string) => { + const { rowCount } = await pool.query(sql` + delete from ${table} + where id=${id} + `); + if (rowCount < 1) { + throw new RequestError({ + code: 'entity.not_exists_with_id', + name: Resources.tableSingular, + id, + status: 404, + }); + } +}; diff --git a/packages/core/src/queries/scopes.ts b/packages/core/src/queries/scopes.ts new file mode 100644 index 000000000..dbd104acd --- /dev/null +++ b/packages/core/src/queries/scopes.ts @@ -0,0 +1,35 @@ +import { ResourceScopeDBEntry, ResourceScopes } from '@logto/schemas'; +import { sql } from 'slonik'; + +import { buildInsertInto } from '@/database/insert-into'; +import pool from '@/database/pool'; +import { convertToIdentifiers } from '@/database/utils'; +import RequestError from '@/errors/RequestError'; + +const { table, fields } = convertToIdentifiers(ResourceScopes); + +export const findAllScopesWithResourceId = async (resourceId: string) => + pool.many(sql` + select ${sql.join(Object.values(fields), sql`,`)} + from ${table} + where ${fields.resourceId}=${resourceId} +`); + +export const insertScope = buildInsertInto(pool, ResourceScopes, { + returning: true, +}); + +export const deleteScopeById = async (id: string) => { + const { rowCount } = await pool.query(sql` + delete from ${table} + where id=${id} + `); + if (rowCount < 1) { + throw new RequestError({ + code: 'entity.not_exists_with_id', + name: ResourceScopes.tableSingular, + id, + status: 404, + }); + } +}; diff --git a/packages/schemas/src/db-entries/custom-types.ts b/packages/schemas/src/db-entries/custom-types.ts index 6cba93da5..0607fafd6 100644 --- a/packages/schemas/src/db-entries/custom-types.ts +++ b/packages/schemas/src/db-entries/custom-types.ts @@ -5,6 +5,12 @@ export enum ApplicationType { SPA = 'SPA', Traditional = 'Traditional', } +export enum SignAlgorithmType { + RS256 = 'RS256', +} +export enum AccessTokenFormatType { + jwt = 'jwt', +} export enum UserLogType { SignInUsernameAndPassword = 'SignInUsernameAndPassword', ExchangeAccessToken = 'ExchangeAccessToken', diff --git a/packages/schemas/src/db-entries/index.ts b/packages/schemas/src/db-entries/index.ts index d8cc7a1db..1b706a96c 100644 --- a/packages/schemas/src/db-entries/index.ts +++ b/packages/schemas/src/db-entries/index.ts @@ -3,5 +3,7 @@ export * from './custom-types'; export * from './application'; export * from './oidc-model-instance'; +export * from './resource-scope'; +export * from './resource'; export * from './user-log'; export * from './user'; diff --git a/packages/schemas/src/db-entries/resource-scope.ts b/packages/schemas/src/db-entries/resource-scope.ts new file mode 100644 index 000000000..7a3d60d32 --- /dev/null +++ b/packages/schemas/src/db-entries/resource-scope.ts @@ -0,0 +1,32 @@ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + +import { z } from 'zod'; + +import { GeneratedSchema, Guard } from '../foundations'; + +export type ResourceScopeDBEntry = { + id: string; + name: string; + description: string; + resourceId: string; +}; + +const guard: Guard = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), + resourceId: z.string(), +}); + +export const ResourceScopes: GeneratedSchema = Object.freeze({ + table: 'resource_scopes', + tableSingular: 'resource_scope', + fields: { + id: 'id', + name: 'name', + description: 'description', + resourceId: 'resource_id', + }, + fieldKeys: ['id', 'name', 'description', 'resourceId'], + guard, +}); diff --git a/packages/schemas/src/db-entries/resource.ts b/packages/schemas/src/db-entries/resource.ts new file mode 100644 index 000000000..e5d2e80b3 --- /dev/null +++ b/packages/schemas/src/db-entries/resource.ts @@ -0,0 +1,39 @@ +// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + +import { z } from 'zod'; + +import { GeneratedSchema, Guard } from '../foundations'; +import { AccessTokenFormatType, SignAlgorithmType } from './custom-types'; + +export type ResourceDBEntry = { + id: string; + name: string; + identifier: string; + accessTokenTtl: number; + accessTokenFormat: AccessTokenFormatType; + signAlgorithm: SignAlgorithmType; +}; + +const guard: Guard = z.object({ + id: z.string(), + name: z.string(), + identifier: z.string(), + accessTokenTtl: z.number(), + accessTokenFormat: z.nativeEnum(AccessTokenFormatType), + signAlgorithm: z.nativeEnum(SignAlgorithmType), +}); + +export const Resources: GeneratedSchema = Object.freeze({ + table: 'resources', + tableSingular: 'resource', + fields: { + id: 'id', + name: 'name', + identifier: 'identifier', + accessTokenTtl: 'access_token_ttl', + accessTokenFormat: 'access_token_format', + signAlgorithm: 'sign_algorithm', + }, + fieldKeys: ['id', 'name', 'identifier', 'accessTokenTtl', 'accessTokenFormat', 'signAlgorithm'], + guard, +}); diff --git a/packages/schemas/src/gen/index.ts b/packages/schemas/src/gen/index.ts index d0c8cd6a6..f2f46907d 100644 --- a/packages/schemas/src/gen/index.ts +++ b/packages/schemas/src/gen/index.ts @@ -45,9 +45,15 @@ const generate = async () => { .split(',') .map((value) => normalizeWhitespaces(value)) .filter((value) => - ['primary', 'foreign', 'unique', 'exclude', 'check'].every( - (constraint) => !value.toLowerCase().startsWith(constraint + ' ') - ) + [ + 'primary', + 'foreign', + 'unique', + 'exclude', + 'check', + 'constraint', + 'references', + ].every((constraint) => !value.toLowerCase().startsWith(constraint + ' ')) ) .map((value) => { const [nameRaw, typeRaw, ...rest] = value.split(' '); diff --git a/packages/schemas/tables/resource_scopes.sql b/packages/schemas/tables/resource_scopes.sql new file mode 100644 index 000000000..6026984fd --- /dev/null +++ b/packages/schemas/tables/resource_scopes.sql @@ -0,0 +1,16 @@ +create table resource_scopes ( + id varchar(24) not null, + name varchar(64) not null, + description text not null, + resource_id varchar(24) not null, + primary key (id), + constraint fk_resource + foreign key (resource_id) + references resources(id) +); + +create unique index resource_scopes__resource_id_name +on resource_scopes ( + resource_id, + name, +); \ No newline at end of file diff --git a/packages/schemas/tables/resources.sql b/packages/schemas/tables/resources.sql new file mode 100644 index 000000000..9e33b8f10 --- /dev/null +++ b/packages/schemas/tables/resources.sql @@ -0,0 +1,12 @@ +create type sign_algorithm_type as enum ('RS256'); +create type access_token_format_type as enum ('jwt'); + +create table resources ( + id varchar(24) not null, + name text not null, + identifier text not null unique, /* resource indicator also used as audience */ + access_token_ttl bigint not null default(86400), /* expiration value in seconds, default is 24h */ + access_token_format access_token_format_type not null default('jwt'), + sign_algorithm sign_algorithm_type not null default('RS256'), + primary key (id) +); \ No newline at end of file