mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(core): addRelationRoutes()
for SchemaRouter
This commit is contained in:
parent
523f6924b6
commit
eae1c5849e
7 changed files with 283 additions and 76 deletions
|
@ -43,6 +43,7 @@
|
|||
"@logto/shared": "workspace:^3.0.0",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"@withtyped/client": "^0.7.22",
|
||||
"camelcase": "^8.0.0",
|
||||
"chalk": "^5.0.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
"date-fns": "^2.29.3",
|
||||
|
@ -72,7 +73,6 @@
|
|||
"otplib": "^12.0.1",
|
||||
"p-retry": "^6.0.0",
|
||||
"pg-protocol": "^1.6.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"redis": "^4.6.5",
|
||||
"roarr": "^7.11.0",
|
||||
|
@ -100,7 +100,6 @@
|
|||
"@types/koa__cors": "^4.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/oidc-provider": "^8.0.0",
|
||||
"@types/pluralize": "^0.0.31",
|
||||
"@types/qrcode": "^1.5.2",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/sinon": "^10.0.13",
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
OrganizationRoles,
|
||||
OrganizationScopes,
|
||||
OrganizationRoleScopeRelations,
|
||||
Users,
|
||||
} from '@logto/schemas';
|
||||
import { type CommonQueryMethods } from 'slonik';
|
||||
|
||||
|
@ -31,6 +32,8 @@ export default class OrganizationQueries extends SchemaQueries<
|
|||
OrganizationRoles,
|
||||
OrganizationScopes
|
||||
),
|
||||
/** Queries for organization - user relations. */
|
||||
users: new RelationQueries(this.pool, 'organization_user_relations', Organizations, Users),
|
||||
};
|
||||
|
||||
constructor(pool: CommonQueryMethods) {
|
||||
|
|
|
@ -51,7 +51,9 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
const router = new SchemaRouter(OrganizationRoles, actions, { disabled: { post: true } });
|
||||
|
||||
/** Allows to carry an initial set of scopes for creating a new organization role. */
|
||||
type CreateOrganizationRolePayload = Omit<CreateOrganizationRole, 'id'> & { scopeIds: string[] };
|
||||
type CreateOrganizationRolePayload = Omit<CreateOrganizationRole, 'id'> & {
|
||||
organizationScopeIds: string[];
|
||||
};
|
||||
|
||||
const createGuard: z.ZodType<CreateOrganizationRolePayload, z.ZodTypeDef, unknown> =
|
||||
OrganizationRoles.createGuard
|
||||
|
@ -59,7 +61,7 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
id: true,
|
||||
})
|
||||
.extend({
|
||||
scopeIds: z.array(z.string()).default([]),
|
||||
organizationScopeIds: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
router.post(
|
||||
|
@ -70,7 +72,7 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
status: [201, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { scopeIds, ...data } = ctx.guard.body;
|
||||
const { organizationScopeIds: scopeIds, ...data } = ctx.guard.body;
|
||||
const role = await actions.post(data);
|
||||
|
||||
if (scopeIds.length > 0) {
|
||||
|
@ -83,63 +85,7 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
);
|
||||
|
||||
// MARK: Role - scope relations routes
|
||||
router.get(
|
||||
'/:id/scopes',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
response: OrganizationScopes.guard.array(),
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id } = ctx.guard.params;
|
||||
|
||||
// Ensure that role exists
|
||||
await actions.getById(id);
|
||||
|
||||
ctx.body = await rolesScopes.getEntries(OrganizationScopes, { organizationRoleId: id });
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:id/scopes',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: z.object({ scopeIds: z.string().min(1).array().nonempty() }),
|
||||
response: OrganizationScopes.guard.array(),
|
||||
status: [200, 404, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
body: { scopeIds },
|
||||
} = ctx.guard;
|
||||
|
||||
await rolesScopes.insert(...scopeIds.map<[string, string]>((scopeId) => [id, scopeId]));
|
||||
|
||||
ctx.body = await rolesScopes.getEntries(OrganizationScopes, { organizationRoleId: id });
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id/scopes/:scopeId',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1), scopeId: z.string().min(1) }),
|
||||
status: [204, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id, scopeId },
|
||||
} = ctx.guard;
|
||||
|
||||
await rolesScopes.delete({ organizationRoleId: id, organizationScopeId: scopeId });
|
||||
|
||||
ctx.status = 204;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
router.addRelationRoutes(OrganizationScopes, rolesScopes, 'scopes');
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { type SchemaLike, type GeneratedSchema, type Guard } from '@logto/schemas';
|
||||
import { generateStandardId, type OmitAutoSetFields } from '@logto/shared';
|
||||
import { type DeepPartial } from '@silverhand/essentials';
|
||||
import camelcase from 'camelcase';
|
||||
import deepmerge from 'deepmerge';
|
||||
import Router, { type IRouterParamContext } from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
@ -8,8 +9,31 @@ import { z } from 'zod';
|
|||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination, { type Pagination } from '#src/middleware/koa-pagination.js';
|
||||
|
||||
import type RelationQueries from './RelationQueries.js';
|
||||
import type SchemaQueries from './SchemaQueries.js';
|
||||
|
||||
/**
|
||||
* Generate the pathname for from a table name.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* tableToPathname('organization_role') // => 'organization-role'
|
||||
* ```
|
||||
*/
|
||||
const tableToPathname = (tableName: string) => tableName.replaceAll('_', '-');
|
||||
|
||||
/**
|
||||
* Generate the camel case schema ID column name.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* camelCaseSchemaId({ tableSingular: 'organization' as const }) // => 'organizationId'
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
const camelCaseSchemaId = <T extends { tableSingular: Table }, Table extends string>(schema: T) =>
|
||||
`${camelcase(schema.tableSingular)}Id` as const;
|
||||
|
||||
/**
|
||||
* Actions configuration for a {@link SchemaRouter}. It contains the
|
||||
* necessary functions to handle the CRUD operations for a schema.
|
||||
|
@ -126,7 +150,7 @@ export default class SchemaRouter<
|
|||
public readonly actions: SchemaActions<Key, CreateSchema, Schema>,
|
||||
config: DeepPartial<SchemaRouterConfig> = {}
|
||||
) {
|
||||
super({ prefix: '/' + schema.table.replaceAll('_', '-') });
|
||||
super({ prefix: '/' + tableToPathname(schema.table) });
|
||||
|
||||
this.config = deepmerge<SchemaRouterConfig, DeepPartial<SchemaRouterConfig>>(
|
||||
{
|
||||
|
@ -220,4 +244,113 @@ export default class SchemaRouter<
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add routes for relations between the current schema and another schema.
|
||||
*
|
||||
* The routes are:
|
||||
*
|
||||
* - `GET /:id/[pathname]`: Get the entities of the relation.
|
||||
* - `POST /:id/[pathname]`: Add entities to the relation.
|
||||
* - `DELETE /:id/[pathname]/:relationSchemaId`: Remove an entity from the relation set.
|
||||
* The `:relationSchemaId` is the entity ID in the relation schema.
|
||||
*
|
||||
* The `[pathname]` is determined by the `pathname` parameter.
|
||||
*
|
||||
* @remarks
|
||||
* The `POST /:id/[pathname]` route accepts a JSON body with the following format:
|
||||
*
|
||||
* ```json
|
||||
* { "[relationSchemaIds]": ["id1", "id2", "id3"] }
|
||||
* ```
|
||||
*
|
||||
* The `[relationSchemaIds]` is the camel case of the relation schema's table name in
|
||||
* singular form with `Ids` suffix. For example, if the relation schema's table name is
|
||||
* `organization_roles`, the `[relationSchemaIds]` will be `organizationRoleIds`.
|
||||
*
|
||||
* @param relationSchema The schema of the relation to be added.
|
||||
* @param relationQueries The queries for the relation.
|
||||
* @param pathname The pathname of the relation. If not provided, it will be
|
||||
* the camel case of the relation schema's table name.
|
||||
* @see {@link RelationQueries} for the `relationQueries` configuration.
|
||||
*/
|
||||
addRelationRoutes<
|
||||
RelationKey extends string,
|
||||
RelationCreateSchema extends Partial<SchemaLike<RelationKey> & { id: string }>,
|
||||
RelationSchema extends SchemaLike<RelationKey> & { id: string },
|
||||
>(
|
||||
relationSchema: GeneratedSchema<RelationKey, RelationCreateSchema, RelationSchema>,
|
||||
relationQueries: RelationQueries<[typeof this.schema, typeof relationSchema]>,
|
||||
pathname = tableToPathname(relationSchema.table)
|
||||
) {
|
||||
const columns = {
|
||||
schemaId: camelCaseSchemaId(this.schema),
|
||||
relationSchemaId: camelCaseSchemaId(relationSchema),
|
||||
relationSchemaIds: camelCaseSchemaId(relationSchema) + 's',
|
||||
};
|
||||
|
||||
this.get(
|
||||
`/:id/${pathname}`,
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
response: relationSchema.guard.array(),
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id } = ctx.guard.params;
|
||||
|
||||
// Ensure that the main entry exists
|
||||
await this.actions.getById(id);
|
||||
|
||||
ctx.body = await relationQueries.getEntries(relationSchema, {
|
||||
[columns.schemaId]: id,
|
||||
});
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
this.post(
|
||||
`/:id/${pathname}`,
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: z.object({ [columns.relationSchemaIds]: z.string().min(1).array().nonempty() }),
|
||||
response: relationSchema.guard.array(),
|
||||
status: [200, 404, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
body: { [columns.relationSchemaIds]: relationIds },
|
||||
} = ctx.guard;
|
||||
|
||||
await relationQueries.insert(
|
||||
...(relationIds?.map<[string, string]>((relationId) => [id, relationId]) ?? [])
|
||||
);
|
||||
|
||||
ctx.body = await relationQueries.getEntries(relationSchema, { [columns.schemaId]: id });
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
this.delete(
|
||||
`/:id/${pathname}/:relationId`,
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1), relationId: z.string().min(1) }),
|
||||
status: [204, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id, relationId },
|
||||
} = ctx.guard;
|
||||
|
||||
await relationQueries.delete({
|
||||
[columns.schemaId]: id,
|
||||
[columns.relationSchemaId]: relationId,
|
||||
});
|
||||
|
||||
ctx.status = 204;
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,14 @@ import { ApiFactory } from './factory.js';
|
|||
|
||||
class OrganizationRoleApi extends ApiFactory<
|
||||
OrganizationRole,
|
||||
{ name: string; description?: string; scopeIds?: string[] }
|
||||
{ name: string; description?: string; organizationScopeIds?: string[] }
|
||||
> {
|
||||
constructor() {
|
||||
super('organization-roles');
|
||||
}
|
||||
|
||||
async addScopes(id: string, scopeIds: string[]): Promise<void> {
|
||||
await authedAdminApi.post(`${this.path}/${id}/scopes`, { json: { scopeIds } });
|
||||
async addScopes(id: string, organizationScopeIds: string[]): Promise<void> {
|
||||
await authedAdminApi.post(`${this.path}/${id}/scopes`, { json: { organizationScopeIds } });
|
||||
}
|
||||
|
||||
async getScopes(id: string): Promise<OrganizationScope[]> {
|
||||
|
|
|
@ -35,8 +35,8 @@ describe('organization role APIs', () => {
|
|||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
]);
|
||||
const scopeIds = [scope1.id, scope2.id];
|
||||
const role = await roleApi.create({ name, scopeIds });
|
||||
const organizationScopeIds = [scope1.id, scope2.id];
|
||||
const role = await roleApi.create({ name, organizationScopeIds });
|
||||
|
||||
expect(role).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
|
|
142
pnpm-lock.yaml
142
pnpm-lock.yaml
|
@ -3175,6 +3175,9 @@ importers:
|
|||
'@withtyped/client':
|
||||
specifier: ^0.7.22
|
||||
version: 0.7.22(zod@3.22.3)
|
||||
camelcase:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
chalk:
|
||||
specifier: ^5.0.0
|
||||
version: 5.1.2
|
||||
|
@ -3262,9 +3265,6 @@ importers:
|
|||
pg-protocol:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
pluralize:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
qrcode:
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3
|
||||
|
@ -3341,9 +3341,6 @@ importers:
|
|||
'@types/oidc-provider':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
'@types/pluralize':
|
||||
specifier: ^0.0.31
|
||||
version: 0.0.31
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.2
|
||||
version: 1.5.2
|
||||
|
@ -3364,7 +3361,7 @@ importers:
|
|||
version: 8.44.0
|
||||
jest:
|
||||
specifier: ^29.5.0
|
||||
version: 29.5.0(@types/node@18.11.18)(ts-node@10.9.1)
|
||||
version: 29.5.0(@types/node@18.11.18)
|
||||
jest-matcher-specific-error:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
|
@ -6905,6 +6902,48 @@ packages:
|
|||
slash: 3.0.0
|
||||
dev: true
|
||||
|
||||
/@jest/core@29.5.0:
|
||||
resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
|
||||
peerDependenciesMeta:
|
||||
node-notifier:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@jest/console': 29.5.0
|
||||
'@jest/reporters': 29.5.0
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/transform': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.5.0
|
||||
jest-config: 29.5.0(@types/node@18.11.18)
|
||||
jest-haste-map: 29.5.0
|
||||
jest-message-util: 29.5.0
|
||||
jest-regex-util: 29.4.3
|
||||
jest-resolve: 29.5.0
|
||||
jest-resolve-dependencies: 29.5.0
|
||||
jest-runner: 29.5.0
|
||||
jest-runtime: 29.5.0
|
||||
jest-snapshot: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
jest-validate: 29.5.0
|
||||
jest-watcher: 29.5.0
|
||||
micromatch: 4.0.5
|
||||
pretty-format: 29.5.0
|
||||
slash: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/@jest/core@29.5.0(ts-node@10.9.1):
|
||||
resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -10823,7 +10862,6 @@ packages:
|
|||
/camelcase@8.0.0:
|
||||
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
|
||||
engines: {node: '>=16'}
|
||||
dev: true
|
||||
|
||||
/caniuse-lite@1.0.30001486:
|
||||
resolution: {integrity: sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==}
|
||||
|
@ -14295,6 +14333,34 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/jest-cli@29.5.0(@types/node@18.11.18):
|
||||
resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
|
||||
peerDependenciesMeta:
|
||||
node-notifier:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@jest/core': 29.5.0
|
||||
'@jest/test-result': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
import-local: 3.1.0
|
||||
jest-config: 29.5.0(@types/node@18.11.18)
|
||||
jest-util: 29.5.0
|
||||
jest-validate: 29.5.0
|
||||
prompts: 2.4.2
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/jest-cli@29.5.0(@types/node@18.11.18)(ts-node@10.9.1):
|
||||
resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -14323,6 +14389,45 @@ packages:
|
|||
- ts-node
|
||||
dev: true
|
||||
|
||||
/jest-config@29.5.0(@types/node@18.11.18):
|
||||
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
ts-node: '>=9.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
ts-node:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.20.2
|
||||
'@jest/test-sequencer': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
babel-jest: 29.5.0(@babel/core@7.20.2)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.5.0
|
||||
jest-environment-node: 29.5.0
|
||||
jest-get-type: 29.4.3
|
||||
jest-regex-util: 29.4.3
|
||||
jest-resolve: 29.5.0
|
||||
jest-runner: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
jest-validate: 29.5.0
|
||||
micromatch: 4.0.5
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.5.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/jest-config@29.5.0(@types/node@18.11.18)(ts-node@10.9.1):
|
||||
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -14734,6 +14839,26 @@ packages:
|
|||
supports-color: 8.1.1
|
||||
dev: true
|
||||
|
||||
/jest@29.5.0(@types/node@18.11.18):
|
||||
resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
|
||||
peerDependenciesMeta:
|
||||
node-notifier:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@jest/core': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
import-local: 3.1.0
|
||||
jest-cli: 29.5.0(@types/node@18.11.18)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/jest@29.5.0(@types/node@18.11.18)(ts-node@10.9.1):
|
||||
resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -17156,6 +17281,7 @@ packages:
|
|||
/pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
|
|
Loading…
Reference in a new issue