From 2016afab72c7bb76052374a2b7209c2d53010e78 Mon Sep 17 00:00:00 2001 From: Brainslug Date: Fri, 19 May 2023 16:39:25 +0200 Subject: [PATCH] Move SDK to monorepo (#18647) * Adding the JS SDK to the core repo * Create tough-crews-shout.md * cleaning up some files * Removed generated files * updated tests to vitest * migrating to vitest * updated api mocking * updated api mocking for tfa * updated comments and fields tests * updated the rest of the handler tests * updated the rest of the tests * fix build output * update license year * moved package to packages/sdk * updated package lock * Move vitest config to 'vitest.config.ts' As we have it in the api (since no vite only vitest) * Sort package.json file As we did for the other packages * remove argon2 dependency --------- Co-authored-by: rijkvanzanten Co-authored-by: Pascal Jufer --- .changeset/tough-crews-shout.md | 6 + packages/sdk/index.mjs | 1 + packages/sdk/license | 7 + packages/sdk/package.json | 63 ++ packages/sdk/readme.md | 61 ++ packages/sdk/src/auth.ts | 43 ++ packages/sdk/src/base/auth.ts | 185 +++++ packages/sdk/src/base/directus.ts | 233 ++++++ packages/sdk/src/base/index.ts | 6 + packages/sdk/src/base/items.ts | 168 +++++ packages/sdk/src/base/storage/base.ts | 86 +++ packages/sdk/src/base/storage/index.ts | 3 + packages/sdk/src/base/storage/localstorage.ts | 28 + packages/sdk/src/base/storage/memory.ts | 30 + packages/sdk/src/base/transport.ts | 123 ++++ packages/sdk/src/directus.ts | 70 ++ packages/sdk/src/handlers/activity.ts | 23 + packages/sdk/src/handlers/assets.ts | 21 + packages/sdk/src/handlers/collections.ts | 58 ++ packages/sdk/src/handlers/comments.ts | 32 + packages/sdk/src/handlers/extensions.ts | 63 ++ packages/sdk/src/handlers/fields.ts | 58 ++ packages/sdk/src/handlers/files.ts | 21 + packages/sdk/src/handlers/folders.ts | 15 + packages/sdk/src/handlers/graphql.ts | 26 + packages/sdk/src/handlers/index.ts | 16 + packages/sdk/src/handlers/invites.ts | 25 + packages/sdk/src/handlers/me.ts | 32 + packages/sdk/src/handlers/passwords.ts | 17 + packages/sdk/src/handlers/permissions.ts | 15 + packages/sdk/src/handlers/presets.ts | 15 + packages/sdk/src/handlers/relations.ts | 49 ++ packages/sdk/src/handlers/revisions.ts | 15 + packages/sdk/src/handlers/roles.ts | 15 + packages/sdk/src/handlers/server.ts | 40 ++ packages/sdk/src/handlers/settings.ts | 14 + packages/sdk/src/handlers/singleton.ts | 31 + packages/sdk/src/handlers/tfa.ts | 24 + packages/sdk/src/handlers/users.ts | 28 + packages/sdk/src/handlers/utils.ts | 40 ++ packages/sdk/src/index.ts | 14 + packages/sdk/src/items.ts | 406 +++++++++++ packages/sdk/src/singleton.ts | 9 + packages/sdk/src/storage.ts | 10 + packages/sdk/src/transport.ts | 88 +++ packages/sdk/src/types.ts | 363 ++++++++++ packages/sdk/tests/base/auth.browser.test.ts | 85 +++ packages/sdk/tests/base/auth.node.test.ts | 74 ++ packages/sdk/tests/base/auth.test.ts | 174 +++++ .../sdk/tests/base/directus.browser.test.ts | 14 + packages/sdk/tests/base/directus.node.ts | 11 + packages/sdk/tests/base/directus.test.ts | 188 +++++ .../tests/base/storage/localstorage.test.ts | 8 + .../sdk/tests/base/storage/memory.test.ts | 8 + packages/sdk/tests/base/storage/tests.ts | 90 +++ packages/sdk/tests/base/transport.test.ts | 152 ++++ packages/sdk/tests/blog.d.ts | 26 + packages/sdk/tests/handlers/comments.test.ts | 65 ++ packages/sdk/tests/handlers/fields.test.ts | 56 ++ packages/sdk/tests/handlers/invites.test.ts | 23 + packages/sdk/tests/handlers/me.test.ts | 26 + packages/sdk/tests/handlers/passwords.test.ts | 20 + packages/sdk/tests/handlers/relations.test.ts | 20 + packages/sdk/tests/handlers/server.test.ts | 40 ++ packages/sdk/tests/handlers/tfa.test.ts | 44 ++ packages/sdk/tests/handlers/utils.test.ts | 81 +++ packages/sdk/tests/items.test.ts | 440 ++++++++++++ packages/sdk/tests/singleton.test.ts | 77 ++ packages/sdk/tests/tsconfig.json | 7 + packages/sdk/tests/utils.ts | 82 +++ packages/sdk/tsconfig.json | 32 + packages/sdk/tsconfig.prod.json | 4 + packages/sdk/vitest.config.ts | 7 + pnpm-lock.yaml | 667 ++++++++++++++++-- 74 files changed, 5149 insertions(+), 68 deletions(-) create mode 100644 .changeset/tough-crews-shout.md create mode 100644 packages/sdk/index.mjs create mode 100644 packages/sdk/license create mode 100644 packages/sdk/package.json create mode 100644 packages/sdk/readme.md create mode 100644 packages/sdk/src/auth.ts create mode 100644 packages/sdk/src/base/auth.ts create mode 100644 packages/sdk/src/base/directus.ts create mode 100644 packages/sdk/src/base/index.ts create mode 100644 packages/sdk/src/base/items.ts create mode 100644 packages/sdk/src/base/storage/base.ts create mode 100644 packages/sdk/src/base/storage/index.ts create mode 100644 packages/sdk/src/base/storage/localstorage.ts create mode 100644 packages/sdk/src/base/storage/memory.ts create mode 100644 packages/sdk/src/base/transport.ts create mode 100644 packages/sdk/src/directus.ts create mode 100644 packages/sdk/src/handlers/activity.ts create mode 100644 packages/sdk/src/handlers/assets.ts create mode 100644 packages/sdk/src/handlers/collections.ts create mode 100644 packages/sdk/src/handlers/comments.ts create mode 100644 packages/sdk/src/handlers/extensions.ts create mode 100644 packages/sdk/src/handlers/fields.ts create mode 100644 packages/sdk/src/handlers/files.ts create mode 100644 packages/sdk/src/handlers/folders.ts create mode 100644 packages/sdk/src/handlers/graphql.ts create mode 100644 packages/sdk/src/handlers/index.ts create mode 100644 packages/sdk/src/handlers/invites.ts create mode 100644 packages/sdk/src/handlers/me.ts create mode 100644 packages/sdk/src/handlers/passwords.ts create mode 100644 packages/sdk/src/handlers/permissions.ts create mode 100644 packages/sdk/src/handlers/presets.ts create mode 100644 packages/sdk/src/handlers/relations.ts create mode 100644 packages/sdk/src/handlers/revisions.ts create mode 100644 packages/sdk/src/handlers/roles.ts create mode 100644 packages/sdk/src/handlers/server.ts create mode 100644 packages/sdk/src/handlers/settings.ts create mode 100644 packages/sdk/src/handlers/singleton.ts create mode 100644 packages/sdk/src/handlers/tfa.ts create mode 100644 packages/sdk/src/handlers/users.ts create mode 100644 packages/sdk/src/handlers/utils.ts create mode 100644 packages/sdk/src/index.ts create mode 100644 packages/sdk/src/items.ts create mode 100644 packages/sdk/src/singleton.ts create mode 100644 packages/sdk/src/storage.ts create mode 100644 packages/sdk/src/transport.ts create mode 100644 packages/sdk/src/types.ts create mode 100644 packages/sdk/tests/base/auth.browser.test.ts create mode 100644 packages/sdk/tests/base/auth.node.test.ts create mode 100644 packages/sdk/tests/base/auth.test.ts create mode 100644 packages/sdk/tests/base/directus.browser.test.ts create mode 100644 packages/sdk/tests/base/directus.node.ts create mode 100644 packages/sdk/tests/base/directus.test.ts create mode 100644 packages/sdk/tests/base/storage/localstorage.test.ts create mode 100644 packages/sdk/tests/base/storage/memory.test.ts create mode 100644 packages/sdk/tests/base/storage/tests.ts create mode 100644 packages/sdk/tests/base/transport.test.ts create mode 100644 packages/sdk/tests/blog.d.ts create mode 100644 packages/sdk/tests/handlers/comments.test.ts create mode 100644 packages/sdk/tests/handlers/fields.test.ts create mode 100644 packages/sdk/tests/handlers/invites.test.ts create mode 100644 packages/sdk/tests/handlers/me.test.ts create mode 100644 packages/sdk/tests/handlers/passwords.test.ts create mode 100644 packages/sdk/tests/handlers/relations.test.ts create mode 100644 packages/sdk/tests/handlers/server.test.ts create mode 100644 packages/sdk/tests/handlers/tfa.test.ts create mode 100644 packages/sdk/tests/handlers/utils.test.ts create mode 100644 packages/sdk/tests/items.test.ts create mode 100644 packages/sdk/tests/singleton.test.ts create mode 100644 packages/sdk/tests/tsconfig.json create mode 100644 packages/sdk/tests/utils.ts create mode 100644 packages/sdk/tsconfig.json create mode 100644 packages/sdk/tsconfig.prod.json create mode 100644 packages/sdk/vitest.config.ts diff --git a/.changeset/tough-crews-shout.md b/.changeset/tough-crews-shout.md new file mode 100644 index 0000000000000..f08786c16fd81 --- /dev/null +++ b/.changeset/tough-crews-shout.md @@ -0,0 +1,6 @@ +--- +"directus": patch +"@directus/sdk": patch +--- + +Moved the JS SDK to the monorepo diff --git a/packages/sdk/index.mjs b/packages/sdk/index.mjs new file mode 100644 index 0000000000000..fb918b0fc73ad --- /dev/null +++ b/packages/sdk/index.mjs @@ -0,0 +1 @@ +export * from './dist/sdk.cjs.js'; diff --git a/packages/sdk/license b/packages/sdk/license new file mode 100644 index 0000000000000..8cac72280665c --- /dev/null +++ b/packages/sdk/license @@ -0,0 +1,7 @@ +Copyright 2023 Monospace Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 0000000000000..22657dd6b75b2 --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,63 @@ +{ + "name": "@directus/sdk", + "version": "10.3.3", + "description": "The official Directus SDK for use in JavaScript!", + "keywords": [ + "api", + "client", + "cms", + "directus", + "headless", + "javascript", + "node", + "sdk" + ], + "bugs": { + "url": "https://github.com/directus/directus/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/directus/directus.git", + "directory": "packages/sdk" + }, + "license": "MIT", + "author": "Rijk van Zanten ", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": { + "node": "./index.mjs", + "default": "./dist/sdk.bundler.js" + }, + "require": "./dist/sdk.cjs.js" + }, + "./package.json": "./package.json" + }, + "main": "dist/sdk.cjs.js", + "unpkg": "dist/sdk.esm.min.js", + "module": "dist/sdk.bundler.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "index.mjs" + ], + "scripts": { + "build": "tsc --project tsconfig.prod.json", + "dev": "tsc --watch", + "test": "vitest --watch=false" + }, + "dependencies": { + "axios": "^0.27.2" + }, + "devDependencies": { + "@directus/tsconfig": "0.0.7", + "@types/node": "^18.0.3", + "@typescript-eslint/eslint-plugin": "^5.30.5", + "@typescript-eslint/parser": "^5.30.5", + "dotenv": "16.0.1", + "jsdom": "^22.0.0", + "msw": "^1.2.1", + "typescript": "4.7.4", + "vitest": "0.31.0" + } +} diff --git a/packages/sdk/readme.md b/packages/sdk/readme.md new file mode 100644 index 0000000000000..b3b7afc66f9fe --- /dev/null +++ b/packages/sdk/readme.md @@ -0,0 +1,61 @@ +# Directus JS SDK + +## Installation + +``` +npm install @directus/sdk +``` + +## Basic Usage + +```js +import { Directus } from '@directus/sdk'; + +const directus = new Directus('http://directus.example.com'); + +const items = await directus.items('articles').readOne(15); +console.log(items); +``` + +```js +import { Directus } from '@directus/sdk'; + +const directus = new Directus('http://directus.example.com'); + +directus + .items('articles') + .readOne(15) + .then((item) => { + console.log(item); + }); +``` + +## Reference + +See [the docs](https://docs.directus.io/reference/sdk/) for a full usage reference and all supported methods. + +## Contributing + +### Requirements + +- NodeJS LTS +- pnpm 7.5.0 or newer + +### Commands + +The following `pnpm` scripts are available: + +- `pnpm lint` – Lint the code using Eslint / Prettier +- `pnpm test` – Run the unit tests + +Make sure that both commands pass locally before creating a Pull Request. + +### Pushing a Release + +_This applies to maintainers only_ + +1. Create a new version / tag by running `pnpm version `. Tip: use `pnpm version patch|minor|major` to + auto-bump the version number +1. Push the version commit / tag to GitHub (`git push && git push --tags`) + +The CI will automatically build and release to npm, and generate the release notes. diff --git a/packages/sdk/src/auth.ts b/packages/sdk/src/auth.ts new file mode 100644 index 0000000000000..ae7e5a799d269 --- /dev/null +++ b/packages/sdk/src/auth.ts @@ -0,0 +1,43 @@ +import { IStorage } from './storage'; +import { ITransport } from './transport'; +import { PasswordsHandler } from './handlers/passwords'; + +export type AuthCredentials = { + email: string; + password: string; + otp?: string; +}; + +export type AuthToken = string; + +export type AuthTokenType = 'DynamicToken' | 'StaticToken' | null; + +export type AuthResult = { + access_token: string; + expires: number; + refresh_token?: string; +}; + +export type AuthMode = 'json' | 'cookie'; + +export type AuthOptions = { + mode?: AuthMode; + autoRefresh?: boolean; + msRefreshBeforeExpires?: number; + staticToken?: string; + transport: ITransport; + storage: IStorage; +}; + +export abstract class IAuth { + mode = (typeof window === 'undefined' ? 'json' : 'cookie') as AuthMode; + + abstract readonly token: Promise; + abstract readonly password: PasswordsHandler; + + abstract login(credentials: AuthCredentials): Promise; + abstract refresh(): Promise; + abstract refreshIfExpired(): Promise; + abstract static(token: AuthToken): Promise; + abstract logout(): Promise; +} diff --git a/packages/sdk/src/base/auth.ts b/packages/sdk/src/base/auth.ts new file mode 100644 index 0000000000000..e186f888b95cf --- /dev/null +++ b/packages/sdk/src/base/auth.ts @@ -0,0 +1,185 @@ +import { IAuth, AuthCredentials, AuthResult, AuthToken, AuthOptions, AuthTokenType } from '../auth'; +import { PasswordsHandler } from '../handlers/passwords'; +import { IStorage } from '../storage'; +import { ITransport } from '../transport'; + +export type AuthStorage = { + access_token: T extends 'DynamicToken' | 'StaticToken' ? string : null; + expires: T extends 'DynamicToken' ? number : null; + refresh_token?: T extends 'DynamicToken' ? string : null; +}; + +export class Auth extends IAuth { + autoRefresh = true; + msRefreshBeforeExpires = 30000; + staticToken = ''; + + private _storage: IStorage; + private _transport: ITransport; + private passwords?: PasswordsHandler; + + private _refreshPromise?: Promise; + + constructor(options: AuthOptions) { + super(); + + this._transport = options.transport; + this._storage = options.storage; + + this.autoRefresh = options?.autoRefresh ?? this.autoRefresh; + this.mode = options?.mode ?? this.mode; + this.msRefreshBeforeExpires = options?.msRefreshBeforeExpires ?? this.msRefreshBeforeExpires; + + if (options?.staticToken) { + this.staticToken = options?.staticToken; + + this.updateStorage<'StaticToken'>({ + access_token: this.staticToken, + expires: null, + refresh_token: null, + }); + } + } + + get storage(): IStorage { + return this._storage; + } + + get transport(): ITransport { + return this._transport; + } + + get token(): Promise { + return (async () => { + if (this._refreshPromise) { + try { + await this._refreshPromise; + } finally { + this._refreshPromise = undefined; + } + } + + return this._storage.auth_token; + })(); + } + + get password(): PasswordsHandler { + return (this.passwords = this.passwords || new PasswordsHandler(this._transport)); + } + + private resetStorage() { + this._storage.auth_token = null; + this._storage.auth_refresh_token = null; + this._storage.auth_expires = null; + this._storage.auth_expires_at = null; + } + + private updateStorage(result: AuthStorage) { + const expires = result.expires ?? null; + this._storage.auth_token = result.access_token; + this._storage.auth_refresh_token = result.refresh_token ?? null; + this._storage.auth_expires = expires; + this._storage.auth_expires_at = new Date().getTime() + (expires ?? 0); + } + + async refreshIfExpired() { + if (this.staticToken) return; + if (!this.autoRefresh) return; + + if (!this._storage.auth_expires_at) { + // wait because resetStorage() call in refresh() + try { + await this._refreshPromise; + } finally { + this._refreshPromise = undefined; + } + + return; + } + + if (this._storage.auth_expires_at < new Date().getTime() + this.msRefreshBeforeExpires) { + this.refresh(); + } + + try { + await this._refreshPromise; // wait for refresh + } finally { + this._refreshPromise = undefined; + } + } + + refresh(): Promise { + const refreshPromise = async () => { + const refresh_token = this._storage.auth_refresh_token; + + this.resetStorage(); + + const response = await this._transport.post('/auth/refresh', { + refresh_token: this.mode === 'json' ? refresh_token : undefined, + }); + + this.updateStorage<'DynamicToken'>(response.data!); + + return { + access_token: response.data!.access_token, + ...(response.data?.refresh_token && { refresh_token: response.data.refresh_token }), + expires: response.data!.expires, + }; + }; + + return (this._refreshPromise = refreshPromise()); + } + + async login(credentials: AuthCredentials): Promise { + this.resetStorage(); + + const response = await this._transport.post( + '/auth/login', + { mode: this.mode, ...credentials }, + { headers: { Authorization: null } } + ); + + this.updateStorage(response.data!); + + return { + access_token: response.data!.access_token, + ...(response.data?.refresh_token && { + refresh_token: response.data.refresh_token, + }), + expires: response.data!.expires, + }; + } + + async static(token: AuthToken): Promise { + if (!this.staticToken) this.staticToken = token; + + await this._transport.get('/users/me', { + params: { access_token: token }, + headers: { Authorization: null }, + }); + + this.updateStorage<'StaticToken'>({ + access_token: token, + expires: null, + refresh_token: null, + }); + + return true; + } + + async logout(): Promise { + let refresh_token: string | undefined; + + if (this.mode === 'json') { + refresh_token = this._storage.auth_refresh_token || undefined; + } + + await this._transport.post('/auth/logout', { refresh_token }); + + this.updateStorage({ + access_token: null, + expires: null, + refresh_token: null, + }); + } +} diff --git a/packages/sdk/src/base/directus.ts b/packages/sdk/src/base/directus.ts new file mode 100644 index 0000000000000..e38c825bc3814 --- /dev/null +++ b/packages/sdk/src/base/directus.ts @@ -0,0 +1,233 @@ +import { IAuth, AuthOptions } from '../auth'; +import { IDirectus } from '../directus'; +import { + ActivityHandler, + AssetsHandler, + CollectionsHandler, + FieldsHandler, + FilesHandler, + FoldersHandler, + PermissionsHandler, + PresetsHandler, + RelationsHandler, + RevisionsHandler, + RolesHandler, + ServerHandler, + SettingsHandler, + UsersHandler, + UtilsHandler, +} from '../handlers'; +import { IItems, Item } from '../items'; +import { ITransport, TransportOptions } from '../transport'; +import { ItemsHandler } from './items'; +import { Transport } from './transport'; +import { Auth } from './auth'; +import { IStorage } from '../storage'; +import { LocalStorage, MemoryStorage, StorageOptions } from './storage'; +import { TypeMap, TypeOf, PartialBy } from '../types'; +import { GraphQLHandler } from '../handlers/graphql'; +import { ISingleton } from '../singleton'; +import { SingletonHandler } from '../handlers/singleton'; + +export type DirectusStorageOptions = StorageOptions & { mode?: 'LocalStorage' | 'MemoryStorage' }; + +export type DirectusOptions = { + auth?: IAuthHandler | PartialBy; + transport?: ITransport | Partial; + storage?: IStorage | DirectusStorageOptions; +}; + +export class Directus implements IDirectus { + private _url: string; + private _options?: DirectusOptions; + private _auth: IAuthHandler; + private _transport: ITransport; + private _storage: IStorage; + private _assets?: AssetsHandler; + private _activity?: ActivityHandler>; + private _collections?: CollectionsHandler>; + private _fields?: FieldsHandler>; + private _files?: FilesHandler>; + private _folders?: FoldersHandler>; + private _permissions?: PermissionsHandler>; + private _presets?: PresetsHandler>; + private _relations?: RelationsHandler>; + private _revisions?: RevisionsHandler>; + private _roles?: RolesHandler>; + private _users?: UsersHandler>; + private _server?: ServerHandler; + private _utils?: UtilsHandler; + private _graphql?: GraphQLHandler; + private _settings?: SettingsHandler>; + + private _items: { + [collection: string]: ItemsHandler; + }; + + private _singletons: { + [collection: string]: SingletonHandler; + }; + + constructor(url: string, options?: DirectusOptions) { + this._url = url; + this._options = options; + this._items = {}; + this._singletons = {}; + + if (this._options?.storage && this._options?.storage instanceof IStorage) this._storage = this._options.storage; + else { + const directusStorageOptions = this._options?.storage as DirectusStorageOptions | undefined; + const { mode, ...storageOptions } = directusStorageOptions ?? {}; + + if (mode === 'MemoryStorage' || typeof window === 'undefined') { + this._storage = new MemoryStorage(storageOptions); + } else { + this._storage = new LocalStorage(storageOptions); + } + } + + if (this._options?.transport && this._options?.transport instanceof ITransport) { + this._transport = this._options.transport; + } else { + this._transport = new Transport({ + url: this.url, + ...this._options?.transport, + beforeRequest: async (config) => { + if (this._url.indexOf('/auth/refresh') === -1 && config.method?.toLowerCase() !== 'post') { + await this._auth.refreshIfExpired(); + } + + const token = this.storage.auth_token; + + let bearer = ''; + + if (token) { + bearer = token.startsWith(`Bearer `) + ? String(this.storage.auth_token) + : `Bearer ${this.storage.auth_token}`; + } + + const authenticatedConfig = { + ...config, + headers: { + Authorization: bearer, + ...config.headers, + }, + }; + + if (!(this._options?.transport instanceof ITransport) && this._options?.transport?.beforeRequest) { + return this._options?.transport?.beforeRequest(authenticatedConfig); + } + + return authenticatedConfig; + }, + }); + } + + if (this._options?.auth && this._options?.auth instanceof IAuth) this._auth = this._options.auth; + else { + this._auth = new Auth({ + transport: this._transport, + storage: this._storage, + ...this._options?.auth, + } as AuthOptions) as unknown as IAuthHandler; + } + } + + get url() { + return this._url; + } + + get auth(): IAuthHandler { + return this._auth; + } + + get storage(): IStorage { + return this._storage; + } + + get transport(): ITransport { + return this._transport; + } + + get assets(): AssetsHandler { + return this._assets || (this._assets = new AssetsHandler(this.transport)); + } + + get activity(): ActivityHandler> { + return this._activity || (this._activity = new ActivityHandler>(this.transport)); + } + + get collections(): CollectionsHandler> { + return ( + this._collections || + (this._collections = new CollectionsHandler>(this.transport)) + ); + } + + get fields(): FieldsHandler> { + return this._fields || (this._fields = new FieldsHandler>(this.transport)); + } + + get files(): FilesHandler> { + return this._files || (this._files = new FilesHandler>(this.transport)); + } + + get folders(): FoldersHandler> { + return this._folders || (this._folders = new FoldersHandler>(this.transport)); + } + + get permissions(): PermissionsHandler> { + return ( + this._permissions || + (this._permissions = new PermissionsHandler>(this.transport)) + ); + } + + get presets(): PresetsHandler> { + return this._presets || (this._presets = new PresetsHandler>(this.transport)); + } + + get relations(): RelationsHandler> { + return this._relations || (this._relations = new RelationsHandler>(this.transport)); + } + + get revisions(): RevisionsHandler> { + return this._revisions || (this._revisions = new RevisionsHandler>(this.transport)); + } + + get roles(): RolesHandler> { + return this._roles || (this._roles = new RolesHandler>(this.transport)); + } + + get users(): UsersHandler> { + return this._users || (this._users = new UsersHandler>(this.transport)); + } + + get settings(): SettingsHandler> { + return this._settings || (this._settings = new SettingsHandler>(this.transport)); + } + + get server(): ServerHandler { + return this._server || (this._server = new ServerHandler(this.transport)); + } + + get utils(): UtilsHandler { + return this._utils || (this._utils = new UtilsHandler(this.transport)); + } + + get graphql(): GraphQLHandler { + return this._graphql || (this._graphql = new GraphQLHandler(this.transport)); + } + + singleton(collection: C): ISingleton { + return ( + this._singletons[collection] || + (this._singletons[collection] = new SingletonHandler(collection, this.transport)) + ); + } + + items(collection: C): IItems { + return this._items[collection] || (this._items[collection] = new ItemsHandler(collection, this.transport)); + } +} diff --git a/packages/sdk/src/base/index.ts b/packages/sdk/src/base/index.ts new file mode 100644 index 0000000000000..6bf9107f219a3 --- /dev/null +++ b/packages/sdk/src/base/index.ts @@ -0,0 +1,6 @@ +export * from './storage'; +export * from './transport'; + +export * from './auth'; +export * from './directus'; +export * from './items'; diff --git a/packages/sdk/src/base/items.ts b/packages/sdk/src/base/items.ts new file mode 100644 index 0000000000000..35988a2745876 --- /dev/null +++ b/packages/sdk/src/base/items.ts @@ -0,0 +1,168 @@ +import { ITransport } from '../transport'; +import { + IItems, + Item, + QueryOne, + QueryMany, + OneItem, + ManyItems, + ItemInput, + ItemsOptions, + EmptyParamError, +} from '../items'; +import { ID, FieldType } from '../types'; + +export class ItemsHandler implements IItems { + protected transport: ITransport; + protected endpoint: string; + protected collection: string; + + constructor(collection: string, transport: ITransport) { + this.collection = collection; + this.transport = transport; + this.endpoint = collection.startsWith('directus_') ? `/${collection.substring(9)}` : `/items/${collection}`; + } + + async readOne>(id: ID, query?: Q, options?: ItemsOptions): Promise> { + if (`${id}` === '') throw new EmptyParamError('id'); + + const response = await this.transport.get>(`${this.endpoint}/${encodeURI(id as string)}`, { + params: query, + ...options?.requestOptions, + }); + + return response.data!; + } + + async readMany>(ids: ID[], query?: Q, options?: ItemsOptions): Promise> { + const collectionFields = await this.transport.get(`/fields/${this.collection}`); + + const primaryKeyField = collectionFields.data?.find((field: any) => field.schema.is_primary_key === true); + + const { data, meta } = await this.transport.get>[]>(`${this.endpoint}`, { + params: { + ...query, + filter: { + [primaryKeyField!.field]: { _in: ids }, + ...query?.filter, + }, + sort: query?.sort || primaryKeyField!.field, + }, + ...options?.requestOptions, + }); + + return { + data, + ...(meta && { meta }), + }; + } + + async readByQuery>(query?: Q, options?: ItemsOptions): Promise> { + const { data, meta } = await this.transport.get>[]>(`${this.endpoint}`, { + params: query, + ...options?.requestOptions, + }); + + return { + data, + ...(meta && { meta }), + }; + } + + async createOne>( + item: ItemInput, + query?: Q, + options?: ItemsOptions + ): Promise> { + return ( + await this.transport.post>(`${this.endpoint}`, item, { + params: query, + ...options?.requestOptions, + }) + ).data; + } + + async createMany>( + items: ItemInput[], + query?: Q, + options?: ItemsOptions + ): Promise> { + return await this.transport.post>[]>(`${this.endpoint}`, items, { + params: query, + ...options?.requestOptions, + }); + } + + async updateOne>( + id: ID, + item: ItemInput, + query?: Q, + options?: ItemsOptions + ): Promise> { + if (`${id}` === '') throw new EmptyParamError('id'); + return ( + await this.transport.patch>(`${this.endpoint}/${encodeURI(id as string)}`, item, { + params: query, + ...options?.requestOptions, + }) + ).data; + } + + async updateMany>( + ids: ID[], + data: ItemInput, + query?: Q, + options?: ItemsOptions + ): Promise> { + return await this.transport.patch>[]>( + `${this.endpoint}`, + { + keys: ids, + data, + }, + { + params: query, + ...options?.requestOptions, + } + ); + } + + async updateBatch>( + items: ItemInput[], + query?: Q, + options?: ItemsOptions + ): Promise> { + return await this.transport.patch>[]>(`${this.endpoint}`, items, { + params: query, + ...options?.requestOptions, + }); + } + + async updateByQuery>( + updateQuery: QueryMany, + data: ItemInput, + query?: Q, + options?: ItemsOptions + ): Promise> { + return await this.transport.patch>[]>( + `${this.endpoint}`, + { + query: updateQuery, + data, + }, + { + params: query, + ...options?.requestOptions, + } + ); + } + + async deleteOne(id: ID, options?: ItemsOptions): Promise { + if (`${id}` === '') throw new EmptyParamError('id'); + await this.transport.delete(`${this.endpoint}/${encodeURI(id as string)}`, undefined, options?.requestOptions); + } + + async deleteMany(ids: ID[], options?: ItemsOptions): Promise { + await this.transport.delete(`${this.endpoint}`, ids, options?.requestOptions); + } +} diff --git a/packages/sdk/src/base/storage/base.ts b/packages/sdk/src/base/storage/base.ts new file mode 100644 index 0000000000000..b2d0488179f2f --- /dev/null +++ b/packages/sdk/src/base/storage/base.ts @@ -0,0 +1,86 @@ +import { IStorage } from '../../storage'; + +export type StorageOptions = { + prefix?: string; +}; + +enum Keys { + AuthToken = 'auth_token', + RefreshToken = 'auth_refresh_token', + Expires = 'auth_expires', + ExpiresAt = 'auth_expires_at', +} + +export abstract class BaseStorage extends IStorage { + protected prefix: string; + + get auth_token(): string | null { + return this.get(Keys.AuthToken); + } + + set auth_token(value: string | null) { + if (value === null) { + this.delete(Keys.AuthToken); + } else { + this.set(Keys.AuthToken, value); + } + } + + get auth_expires(): number | null { + const value = this.get(Keys.Expires); + + if (value === null) { + return null; + } + + return parseInt(value); + } + + set auth_expires(value: number | null) { + if (value === null) { + this.delete(Keys.Expires); + } else { + this.set(Keys.Expires, value!.toString()); + } + } + + get auth_expires_at(): number | null { + const value = this.get(Keys.ExpiresAt); + + if (value === null) { + return null; + } + + return parseInt(value); + } + + set auth_expires_at(value: number | null) { + if (value === null) { + this.delete(Keys.ExpiresAt); + } else { + this.set(Keys.ExpiresAt, value!.toString()); + } + } + + get auth_refresh_token(): string | null { + return this.get(Keys.RefreshToken); + } + + set auth_refresh_token(value: string | null) { + if (value === null) { + this.delete(Keys.RefreshToken); + } else { + this.set(Keys.RefreshToken, value); + } + } + + abstract get(key: string): string | null; + abstract set(key: string, value: string): string; + abstract delete(key: string): string | null; + + constructor(options?: StorageOptions) { + super(); + + this.prefix = options?.prefix ?? ''; + } +} diff --git a/packages/sdk/src/base/storage/index.ts b/packages/sdk/src/base/storage/index.ts new file mode 100644 index 0000000000000..8b5add0743711 --- /dev/null +++ b/packages/sdk/src/base/storage/index.ts @@ -0,0 +1,3 @@ +export * from './base'; +export * from './memory'; +export * from './localstorage'; diff --git a/packages/sdk/src/base/storage/localstorage.ts b/packages/sdk/src/base/storage/localstorage.ts new file mode 100644 index 0000000000000..d71d3d52e63a0 --- /dev/null +++ b/packages/sdk/src/base/storage/localstorage.ts @@ -0,0 +1,28 @@ +import { BaseStorage } from './base'; + +export class LocalStorage extends BaseStorage { + get(key: string): string | null { + const value = localStorage.getItem(this.key(key)); + + if (value !== null) { + return value; + } + + return null; + } + + set(key: string, value: string): string { + localStorage.setItem(this.key(key), value); + return value; + } + + delete(key: string): string | null { + const value = this.get(key); + localStorage.removeItem(this.key(key)); + return value; + } + + private key(name: string): string { + return `${this.prefix}${name}`; + } +} diff --git a/packages/sdk/src/base/storage/memory.ts b/packages/sdk/src/base/storage/memory.ts new file mode 100644 index 0000000000000..ebc6f17a30b35 --- /dev/null +++ b/packages/sdk/src/base/storage/memory.ts @@ -0,0 +1,30 @@ +import { BaseStorage } from './base'; + +export class MemoryStorage extends BaseStorage { + private values: Record = {}; + + get(key: string): string | null { + const k = this.key(key); + + if (k in this.values) { + return this.values[k]!; + } + + return null; + } + + set(key: string, value: string): string { + this.values[this.key(key)] = value; + return value; + } + + delete(key: string): string | null { + const value = this.get(key); + delete this.values[this.key(key)]; + return value; + } + + private key(name: string): string { + return `${this.prefix}${name}`; + } +} diff --git a/packages/sdk/src/base/transport.ts b/packages/sdk/src/base/transport.ts new file mode 100644 index 0000000000000..90688d77e7d24 --- /dev/null +++ b/packages/sdk/src/base/transport.ts @@ -0,0 +1,123 @@ +import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios'; +import { ITransport, TransportMethods, TransportResponse, TransportError, TransportOptions } from '../transport'; + +/** + * Transport implementation + */ +export class Transport extends ITransport { + private axios: AxiosInstance; + private config: TransportOptions; + + constructor(config: TransportOptions) { + super(); + + this.config = config; + + this.axios = axios.create({ + baseURL: this.config.url, + params: this.config.params, + headers: this.config.headers, + onUploadProgress: this.config.onUploadProgress, + maxBodyLength: this.config.maxBodyLength, + maxContentLength: this.config.maxContentLength, + withCredentials: true, + }); + + if (this.config?.beforeRequest) this.beforeRequest = this.config.beforeRequest; + } + + async beforeRequest(config: AxiosRequestConfig): Promise { + return config; + } + + get url(): string { + return this.config.url; + } + + protected async request( + method: TransportMethods, + path: string, + data?: Record, + options?: Omit + ): Promise> { + try { + let config: AxiosRequestConfig = { + method, + url: path, + data: data, + params: options?.params, + headers: options?.headers, + responseType: options?.responseType, + onUploadProgress: options?.onUploadProgress, + }; + + config = await this.beforeRequest(config); + + const response = await this.axios.request(config); + + const content = { + raw: response.data as any, + status: response.status, + statusText: response.statusText, + headers: response.headers, + data: response.data.data, + meta: response.data.meta, + errors: response.data.errors, + }; + + if (response.data.errors) { + throw new TransportError(null, content); + } + + return content; + } catch (err: any) { + if (!err || err instanceof Error === false) { + throw err; + } + + if (axios.isAxiosError(err)) { + const data = err.response?.data as any; + + throw new TransportError(err as AxiosError, { + raw: err.response?.data, + status: err.response?.status, + statusText: err.response?.statusText, + headers: err.response?.headers, + data: data?.data, + meta: data?.meta, + errors: data?.errors, + }); + } + + throw new TransportError(err as Error); + } + } + + async get(path: string, options?: TransportOptions): Promise> { + return await this.request('get', path, undefined, options); + } + + async head(path: string, options?: TransportOptions): Promise> { + return await this.request('head', path, undefined, options); + } + + async options(path: string, options?: TransportOptions): Promise> { + return await this.request('options', path, undefined, options); + } + + async delete(path: string, data?: D, options?: TransportOptions): Promise> { + return await this.request('delete', path, data, options); + } + + async put(path: string, data?: D, options?: TransportOptions): Promise> { + return await this.request('put', path, data, options); + } + + async post(path: string, data?: D, options?: TransportOptions): Promise> { + return await this.request('post', path, data, options); + } + + async patch(path: string, data?: D, options?: TransportOptions): Promise> { + return await this.request('patch', path, data, options); + } +} diff --git a/packages/sdk/src/directus.ts b/packages/sdk/src/directus.ts new file mode 100644 index 0000000000000..3b48e11846d91 --- /dev/null +++ b/packages/sdk/src/directus.ts @@ -0,0 +1,70 @@ +import { IAuth } from './auth'; +import { + ActivityHandler, + AssetsHandler, + CollectionsHandler, + FieldsHandler, + FilesHandler, + FoldersHandler, + PermissionsHandler, + PresetsHandler, + RelationsHandler, + RevisionsHandler, + RolesHandler, + ServerHandler, + SettingsHandler, + UsersHandler, +} from './handlers'; + +import { IItems, Item } from './items'; +import { ITransport } from './transport'; +import { UtilsHandler } from './handlers/utils'; +import { IStorage } from './storage'; +import { TypeMap, TypeOf } from './types'; +import { GraphQLHandler } from './handlers/graphql'; +import { ISingleton } from './singleton'; + +export type DirectusTypes = { + activity: undefined; + assets: undefined; + collections: undefined; + fields: undefined; + files: undefined; + folders: undefined; + permissions: undefined; + presets: undefined; + relations: undefined; + revisions: undefined; + roles: undefined; + settings: undefined; + users: undefined; +}; + +export interface IDirectusBase { + readonly url: string; + readonly auth: IAuth; + readonly storage: IStorage; + readonly transport: ITransport; + readonly server: ServerHandler; + readonly utils: UtilsHandler; + readonly graphql: GraphQLHandler; +} + +export interface IDirectus extends IDirectusBase { + readonly activity: ActivityHandler>; + readonly assets: AssetsHandler; + readonly collections: CollectionsHandler>; + readonly files: FilesHandler>; + readonly fields: FieldsHandler>; + readonly folders: FoldersHandler>; + readonly permissions: PermissionsHandler>; + readonly presets: PresetsHandler>; + readonly revisions: RevisionsHandler>; + readonly relations: RelationsHandler>; + readonly roles: RolesHandler>; + readonly users: UsersHandler>; + readonly settings: SettingsHandler>; + + items(collection: C): IItems; + singleton(collection: C): ISingleton; +} diff --git a/packages/sdk/src/handlers/activity.ts b/packages/sdk/src/handlers/activity.ts new file mode 100644 index 0000000000000..32f7e0f5a6461 --- /dev/null +++ b/packages/sdk/src/handlers/activity.ts @@ -0,0 +1,23 @@ +/** + * Activity handler + */ + +import { ItemsHandler } from '../base/items'; +import { ITransport } from '../transport'; +import { ActivityType, DefaultType } from '../types'; +import { CommentsHandler } from './comments'; + +export type ActivityItem = ActivityType & T; + +export class ActivityHandler extends ItemsHandler> { + private _comments: CommentsHandler; + + constructor(transport: ITransport) { + super('directus_activity', transport); + this._comments = new CommentsHandler(this.transport); + } + + get comments(): CommentsHandler { + return this._comments; + } +} diff --git a/packages/sdk/src/handlers/assets.ts b/packages/sdk/src/handlers/assets.ts new file mode 100644 index 0000000000000..ea3e516badb8e --- /dev/null +++ b/packages/sdk/src/handlers/assets.ts @@ -0,0 +1,21 @@ +import { EmptyParamError } from '../items'; +import { ITransport } from '../transport'; +import { ID } from '../types'; + +export class AssetsHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + async readOne(id: ID): Promise { + if (`${id}` === '') throw new EmptyParamError('id'); + + const response = await this.transport.get(`/assets/${id}`, { + responseType: 'stream', + }); + + return response.raw; + } +} diff --git a/packages/sdk/src/handlers/collections.ts b/packages/sdk/src/handlers/collections.ts new file mode 100644 index 0000000000000..b7c3c7d948ba1 --- /dev/null +++ b/packages/sdk/src/handlers/collections.ts @@ -0,0 +1,58 @@ +/** + * Collections handler + */ + +import { ManyItems, OneItem, ItemInput, QueryOne, EmptyParamError } from '../items'; +import { ITransport } from '../transport'; +import { CollectionType, DefaultType } from '../types'; + +export type CollectionItem = CollectionType & T; + +export class CollectionsHandler { + transport: ITransport; + constructor(transport: ITransport) { + this.transport = transport; + } + + async readOne(collection: string): Promise>> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + const response = await this.transport.get(`/collections/${collection}`); + return response.data as OneItem>; + } + + async readAll(): Promise>> { + const { data, meta } = await this.transport.get(`/collections`); + + return { + data, + meta, + }; + } + + async createOne(collection: ItemInput): Promise>> { + return (await this.transport.post>>(`/collections`, collection)).data; + } + + async createMany(collections: ItemInput[]): Promise>> { + const { data, meta } = await this.transport.post(`/collections`, collections); + + return { + data, + meta, + }; + } + + async updateOne(collection: string, item: ItemInput, query?: QueryOne): Promise>> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + return ( + await this.transport.patch>>(`/collections/${collection}`, item, { + params: query, + }) + ).data; + } + + async deleteOne(collection: string): Promise { + if (`${collection}` === '') throw new EmptyParamError('collection'); + await this.transport.delete(`/collections/${collection}`); + } +} diff --git a/packages/sdk/src/handlers/comments.ts b/packages/sdk/src/handlers/comments.ts new file mode 100644 index 0000000000000..9268286c30210 --- /dev/null +++ b/packages/sdk/src/handlers/comments.ts @@ -0,0 +1,32 @@ +import { Comment, ID } from '../types'; +import { ITransport } from '../transport'; +import { ActivityItem } from './activity'; +import { EmptyParamError } from '../items'; + +export class CommentsHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + async create(comment: Comment): Promise> { + const response = await this.transport.post('/activity/comment', comment); + return response.data; + } + + async update(comment_activity_id: ID, comment: string): Promise> { + if (`${comment_activity_id}` === '') throw new EmptyParamError('comment_activity_id'); + + const response = await this.transport.patch(`/activity/comment/${encodeURI(comment_activity_id as string)}`, { + comment, + }); + + return response.data; + } + + async delete(comment_activity_id: ID): Promise { + if (`${comment_activity_id}` === '') throw new EmptyParamError('comment_activity_id'); + await this.transport.delete(`/activity/comment/${encodeURI(comment_activity_id as string)}`); + } +} diff --git a/packages/sdk/src/handlers/extensions.ts b/packages/sdk/src/handlers/extensions.ts new file mode 100644 index 0000000000000..2fef2d0e591a6 --- /dev/null +++ b/packages/sdk/src/handlers/extensions.ts @@ -0,0 +1,63 @@ +/** + * Settings handler + */ + +import { ITransport, TransportOptions, TransportResponse } from '../transport'; + +export class ExtensionEndpoint implements ITransport { + private name: string; + private transport: ITransport; + + constructor(transport: ITransport, name: string) { + this.name = name; + this.transport = transport; + } + + private endpoint(path: string) { + if (path.startsWith('/')) { + path = path.substr(1); + } + + return `/custom/${this.name}/${path}`; + } + + get(path: string, options?: TransportOptions): Promise> { + return this.transport.get(this.endpoint(path), options); + } + + head(path: string, options?: TransportOptions): Promise> { + return this.transport.head(this.endpoint(path), options); + } + + options(path: string, options?: TransportOptions): Promise> { + return this.transport.options(this.endpoint(path), options); + } + + delete(path: string, data?: P, options?: TransportOptions): Promise> { + return this.transport.delete(this.endpoint(path), data, options); + } + + post(path: string, data?: P, options?: TransportOptions): Promise> { + return this.transport.post(this.endpoint(path), data, options); + } + + put(path: string, data?: P, options?: TransportOptions): Promise> { + return this.transport.put(this.endpoint(path), data, options); + } + + patch(path: string, data?: P, options?: TransportOptions): Promise> { + return this.transport.patch(this.endpoint(path), data, options); + } +} + +export class ExtensionHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + endpoint(name: string): ExtensionEndpoint { + return new ExtensionEndpoint(this.transport, name); + } +} diff --git a/packages/sdk/src/handlers/fields.ts b/packages/sdk/src/handlers/fields.ts new file mode 100644 index 0000000000000..27fa55ef14e9d --- /dev/null +++ b/packages/sdk/src/handlers/fields.ts @@ -0,0 +1,58 @@ +/** + * Fields handler + */ + +import { ManyItems, OneItem, ItemInput, EmptyParamError, DefaultItem } from '../items'; +import { ITransport } from '../transport'; +import { FieldType, DefaultType, ID } from '../types'; + +export type FieldItem = FieldType & T; + +export class FieldsHandler { + transport: ITransport; + constructor(transport: ITransport) { + this.transport = transport; + } + + async readOne(collection: string, id: ID): Promise>> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + if (`${id}` === '') throw new EmptyParamError('id'); + const response = await this.transport.get(`/fields/${collection}/${id}`); + return response.data as OneItem>; + } + + async readMany(collection: string): Promise>> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + const response = await this.transport.get(`/fields/${collection}`); + + return { + data: response.data as DefaultItem>[], + meta: undefined, + }; + } + + async readAll(): Promise>> { + const response = await this.transport.get(`/fields`); + return { + data: response.data as DefaultItem>[], + meta: undefined, + }; + } + + async createOne(collection: string, item: ItemInput): Promise>> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + return (await this.transport.post>>(`/fields/${collection}`, item)).data; + } + + async updateOne(collection: string, field: string, item: ItemInput): Promise>> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + if (`${field}` === '') throw new EmptyParamError('field'); + return (await this.transport.patch>>(`/fields/${collection}/${field}`, item)).data; + } + + async deleteOne(collection: string, field: string): Promise { + if (`${collection}` === '') throw new EmptyParamError('collection'); + if (`${field}` === '') throw new EmptyParamError('field'); + await this.transport.delete(`/fields/${collection}/${field}`); + } +} diff --git a/packages/sdk/src/handlers/files.ts b/packages/sdk/src/handlers/files.ts new file mode 100644 index 0000000000000..117c177bad5c6 --- /dev/null +++ b/packages/sdk/src/handlers/files.ts @@ -0,0 +1,21 @@ +/** + * Files handler + */ + +import { ItemsHandler } from '../base/items'; +import { OneItem, ItemInput } from '../items'; +import { ITransport } from '../transport'; +import { FileType, DefaultType } from '../types'; + +export type FileItem = FileType & T; + +export class FilesHandler extends ItemsHandler> { + constructor(transport: ITransport) { + super('directus_files', transport); + } + + async import(body: { url: string; data?: ItemInput }): Promise>> { + const response = await this.transport.post(`/files/import`, body); + return response.data as OneItem>; + } +} diff --git a/packages/sdk/src/handlers/folders.ts b/packages/sdk/src/handlers/folders.ts new file mode 100644 index 0000000000000..585a14708349f --- /dev/null +++ b/packages/sdk/src/handlers/folders.ts @@ -0,0 +1,15 @@ +/** + * Folders handler + */ + +import { ItemsHandler } from '../base/items'; +import { ITransport } from '../transport'; +import { FolderType, DefaultType } from '../types'; + +export type FolderItem = FolderType & T; + +export class FoldersHandler extends ItemsHandler> { + constructor(transport: ITransport) { + super('directus_folders', transport); + } +} diff --git a/packages/sdk/src/handlers/graphql.ts b/packages/sdk/src/handlers/graphql.ts new file mode 100644 index 0000000000000..96033b7596c77 --- /dev/null +++ b/packages/sdk/src/handlers/graphql.ts @@ -0,0 +1,26 @@ +import { ITransport, TransportResponse } from '../transport'; + +export class GraphQLHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + private async request(base: string, query: string, variables?: any): Promise> { + return await this.transport.post(base, { + query, + variables: typeof variables === 'undefined' ? {} : variables, + }); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async items(query: string, variables?: any): Promise> { + return await this.request('/graphql', query, variables); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async system(query: string, variables?: any): Promise> { + return await this.request('/graphql/system', query, variables); + } +} diff --git a/packages/sdk/src/handlers/index.ts b/packages/sdk/src/handlers/index.ts new file mode 100644 index 0000000000000..1d0bf15359bcd --- /dev/null +++ b/packages/sdk/src/handlers/index.ts @@ -0,0 +1,16 @@ +export * from './activity'; +export * from './assets'; +export * from './comments'; +export * from './collections'; +export * from './fields'; +export * from './files'; +export * from './folders'; +export * from './permissions'; +export * from './presets'; +export * from './relations'; +export * from './revisions'; +export * from './roles'; +export * from './server'; +export * from './settings'; +export * from './users'; +export * from './utils'; diff --git a/packages/sdk/src/handlers/invites.ts b/packages/sdk/src/handlers/invites.ts new file mode 100644 index 0000000000000..ac0c5d0f37cbc --- /dev/null +++ b/packages/sdk/src/handlers/invites.ts @@ -0,0 +1,25 @@ +import { ITransport } from '../transport'; +import { ID } from '../types'; + +export class InvitesHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + async send(email: string, role: ID, invite_url?: string): Promise { + await this.transport.post('/users/invite', { + email, + role, + invite_url, + }); + } + + async accept(token: ID, password: string): Promise { + await this.transport.post(`/users/invite/accept`, { + token, + password, + }); + } +} diff --git a/packages/sdk/src/handlers/me.ts b/packages/sdk/src/handlers/me.ts new file mode 100644 index 0000000000000..1395e819af4ba --- /dev/null +++ b/packages/sdk/src/handlers/me.ts @@ -0,0 +1,32 @@ +import { ItemInput, QueryOne } from '../items'; +import { ITransport } from '../transport'; +import { TFAHandler } from './tfa'; + +export class MeHandler { + private _transport: ITransport; + private _tfa?: TFAHandler; + + constructor(transport: ITransport) { + this._transport = transport; + } + + get tfa(): TFAHandler { + return this._tfa || (this._tfa = new TFAHandler(this._transport)); + } + + async read(query?: QueryOne): Promise> { + const response = await this._transport.get('/users/me', { + params: query, + }); + + return response.data!; + } + + async update(data: ItemInput, query?: QueryOne): Promise> { + const response = await this._transport.patch(`/users/me`, data, { + params: query, + }); + + return response.data!; + } +} diff --git a/packages/sdk/src/handlers/passwords.ts b/packages/sdk/src/handlers/passwords.ts new file mode 100644 index 0000000000000..096b333ee6273 --- /dev/null +++ b/packages/sdk/src/handlers/passwords.ts @@ -0,0 +1,17 @@ +import { ITransport } from '../transport'; + +export class PasswordsHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + async request(email: string, reset_url?: string | null): Promise { + await this.transport.post('/auth/password/request', { email, reset_url }); + } + + async reset(token: string, password: string): Promise { + await this.transport.post('/auth/password/reset', { token, password }); + } +} diff --git a/packages/sdk/src/handlers/permissions.ts b/packages/sdk/src/handlers/permissions.ts new file mode 100644 index 0000000000000..42ab2efe51367 --- /dev/null +++ b/packages/sdk/src/handlers/permissions.ts @@ -0,0 +1,15 @@ +/** + * Permissions handler + */ + +import { ItemsHandler } from '../base/items'; +import { ITransport } from '../transport'; +import { PermissionType, DefaultType } from '../types'; + +export type PermissionItem = PermissionType & T; + +export class PermissionsHandler extends ItemsHandler> { + constructor(transport: ITransport) { + super('directus_permissions', transport); + } +} diff --git a/packages/sdk/src/handlers/presets.ts b/packages/sdk/src/handlers/presets.ts new file mode 100644 index 0000000000000..17b7f568a44f7 --- /dev/null +++ b/packages/sdk/src/handlers/presets.ts @@ -0,0 +1,15 @@ +/** + * Presets handler + */ + +import { ItemsHandler } from '../base/items'; +import { ITransport } from '../transport'; +import { PresetType, DefaultType } from '../types'; + +export type PresetItem = PresetType & T; + +export class PresetsHandler extends ItemsHandler> { + constructor(transport: ITransport) { + super('directus_presets', transport); + } +} diff --git a/packages/sdk/src/handlers/relations.ts b/packages/sdk/src/handlers/relations.ts new file mode 100644 index 0000000000000..21de3b997835b --- /dev/null +++ b/packages/sdk/src/handlers/relations.ts @@ -0,0 +1,49 @@ +/** + * Relations handler + */ +import { ManyItems, OneItem, ItemInput, EmptyParamError } from '../items'; +import { ITransport } from '../transport'; +import { RelationType, DefaultType, ID } from '../types'; + +export type RelationItem = RelationType & T; +export class RelationsHandler { + transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + async readOne(collection: string, id: ID): Promise> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + if (`${id}` === '') throw new EmptyParamError('id'); + const response = await this.transport.get(`/relations/${collection}/${id}`); + return response.data as OneItem; + } + + async readMany(collection: string): Promise> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + const response = await this.transport.get(`/relations/${collection}`); + return response.data; + } + + async readAll(): Promise> { + const response = await this.transport.get(`/relations`); + return response.data; + } + + async createOne(item: ItemInput): Promise> { + return (await this.transport.post>(`/relations`, item)).data; + } + + async updateOne(collection: string, field: string, item: ItemInput): Promise> { + if (`${collection}` === '') throw new EmptyParamError('collection'); + if (`${field}` === '') throw new EmptyParamError('field'); + return (await this.transport.patch>(`/relations/${collection}/${field}`, item)).data; + } + + async deleteOne(collection: string, field: string): Promise { + if (`${collection}` === '') throw new EmptyParamError('collection'); + if (`${field}` === '') throw new EmptyParamError('field'); + await this.transport.delete(`/relations/${collection}/${field}`); + } +} diff --git a/packages/sdk/src/handlers/revisions.ts b/packages/sdk/src/handlers/revisions.ts new file mode 100644 index 0000000000000..edf8b7c0d9ba1 --- /dev/null +++ b/packages/sdk/src/handlers/revisions.ts @@ -0,0 +1,15 @@ +/** + * Revisions handler + */ + +import { ItemsHandler } from '../base/items'; +import { ITransport } from '../transport'; +import { RevisionType, DefaultType } from '../types'; + +export type RevisionItem = RevisionType & T; + +export class RevisionsHandler extends ItemsHandler> { + constructor(transport: ITransport) { + super('directus_revisions', transport); + } +} diff --git a/packages/sdk/src/handlers/roles.ts b/packages/sdk/src/handlers/roles.ts new file mode 100644 index 0000000000000..3c1606a94c5d4 --- /dev/null +++ b/packages/sdk/src/handlers/roles.ts @@ -0,0 +1,15 @@ +/** + * Roles handler + */ + +import { ItemsHandler } from '../base/items'; +import { ITransport } from '../transport'; +import { RoleType, DefaultType } from '../types'; + +export type RoleItem = RoleType & T; + +export class RolesHandler extends ItemsHandler> { + constructor(transport: ITransport) { + super('directus_roles', transport); + } +} diff --git a/packages/sdk/src/handlers/server.ts b/packages/sdk/src/handlers/server.ts new file mode 100644 index 0000000000000..398fad9f1d56e --- /dev/null +++ b/packages/sdk/src/handlers/server.ts @@ -0,0 +1,40 @@ +/** + * Server handler + */ + +import { ITransport } from '../transport'; + +export type ServerInfo = { + project: { + project_name: string; + project_logo: string | null; + project_color: string; + public_foreground: string | null; + public_background: string | null; + public_note: string; + custom_css: string; + }; + directus?: { + version: string; + }; +}; + +export class ServerHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + async ping(): Promise<'pong'> { + return (await this.transport.get('/server/ping')).raw; + } + + async info(): Promise { + return (await this.transport.get('/server/info')).data!; + } + + async oas(): Promise { + return (await this.transport.get('/server/specs/oas')).raw; + } +} diff --git a/packages/sdk/src/handlers/settings.ts b/packages/sdk/src/handlers/settings.ts new file mode 100644 index 0000000000000..bbc487600c005 --- /dev/null +++ b/packages/sdk/src/handlers/settings.ts @@ -0,0 +1,14 @@ +/** + * Settings handler + */ +import { ITransport } from '../transport'; +import { SettingType, DefaultType } from '../types'; +import { SingletonHandler } from './singleton'; + +export type SettingItem = SettingType & T; + +export class SettingsHandler extends SingletonHandler { + constructor(transport: ITransport) { + super('directus_settings', transport); + } +} diff --git a/packages/sdk/src/handlers/singleton.ts b/packages/sdk/src/handlers/singleton.ts new file mode 100644 index 0000000000000..f8338d1e58e5a --- /dev/null +++ b/packages/sdk/src/handlers/singleton.ts @@ -0,0 +1,31 @@ +import { ITransport } from '../transport'; +import { QueryOne, OneItem, ItemInput } from '../items'; +import { ISingleton } from '../singleton'; + +export class SingletonHandler implements ISingleton { + protected collection: string; + protected transport: ITransport; + protected endpoint: string; + + constructor(collection: string, transport: ITransport) { + this.collection = collection; + this.transport = transport; + this.endpoint = collection.startsWith('directus_') ? `/${collection.substring(9)}` : `/items/${collection}`; + } + + async read>(query?: Q): Promise> { + const item = await this.transport.get>(`${this.endpoint}`, { + params: query, + }); + + return item.data; + } + + async update>(data: ItemInput, _query?: Q): Promise> { + const item = await this.transport.patch>(`${this.endpoint}`, data, { + params: _query, + }); + + return item.data; + } +} diff --git a/packages/sdk/src/handlers/tfa.ts b/packages/sdk/src/handlers/tfa.ts new file mode 100644 index 0000000000000..6418dd2d5ebe8 --- /dev/null +++ b/packages/sdk/src/handlers/tfa.ts @@ -0,0 +1,24 @@ +import { ITransport } from '../transport'; +import { TfaType, DefaultType } from '../types'; + +type TfaItem = TfaType & T; +export class TFAHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + async generate(password: string): Promise { + const result = await this.transport.post('/users/me/tfa/generate', { password }); + return result.data; + } + + async enable(secret: string, otp: string): Promise { + await this.transport.post('/users/me/tfa/enable', { secret, otp }); + } + + async disable(otp: string): Promise { + await this.transport.post('/users/me/tfa/disable', { otp }); + } +} diff --git a/packages/sdk/src/handlers/users.ts b/packages/sdk/src/handlers/users.ts new file mode 100644 index 0000000000000..4709bd1e51830 --- /dev/null +++ b/packages/sdk/src/handlers/users.ts @@ -0,0 +1,28 @@ +/** + * Users handler + */ + +import { ItemsHandler } from '../base/items'; +import { ITransport } from '../transport'; +import { DefaultType, UserType } from '../types'; +import { InvitesHandler } from './invites'; +import { MeHandler } from './me'; + +export type UserItem = UserType & T; + +export class UsersHandler extends ItemsHandler> { + private _invites?: InvitesHandler; + private _me?: MeHandler>; + + constructor(transport: ITransport) { + super('directus_users', transport); + } + + get invites(): InvitesHandler { + return this._invites || (this._invites = new InvitesHandler(this.transport)); + } + + get me(): MeHandler> { + return this._me || (this._me = new MeHandler(this.transport)); + } +} diff --git a/packages/sdk/src/handlers/utils.ts b/packages/sdk/src/handlers/utils.ts new file mode 100644 index 0000000000000..b703ad91402d6 --- /dev/null +++ b/packages/sdk/src/handlers/utils.ts @@ -0,0 +1,40 @@ +/** + * Utils handler + */ + +import { ITransport } from '../transport'; +import { ID } from '../types'; + +export class UtilsHandler { + private transport: ITransport; + + constructor(transport: ITransport) { + this.transport = transport; + } + + random = { + string: async (length = 32): Promise => { + const result = await this.transport.get('/utils/random/string', { params: { length } }); + return result.data!; + }, + }; + + hash = { + generate: async (string: string): Promise => { + const result = await this.transport.post('/utils/hash/generate', { string }); + return result.data!; + }, + verify: async (string: string, hash: string): Promise => { + const result = await this.transport.post('/utils/hash/verify', { string, hash }); + return result.data!; + }, + }; + + async sort(collection: string, item: ID, to: ID): Promise { + await this.transport.post(`/utils/sort/${encodeURI(collection as string)}`, { item, to }); + } + + async revert(revision: ID): Promise { + await this.transport.post(`/utils/revert/${encodeURI(revision as string)}`); + } +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts new file mode 100644 index 0000000000000..180bb1ce9458e --- /dev/null +++ b/packages/sdk/src/index.ts @@ -0,0 +1,14 @@ +// Interfaces +export * from './auth'; +export * from './directus'; +export * from './handlers'; +export * from './items'; +export * from './singleton'; +export * from './storage'; +export * from './transport'; + +// Implementations +export * from './base'; + +// Types +export * from './types'; diff --git a/packages/sdk/src/items.ts b/packages/sdk/src/items.ts new file mode 100644 index 0000000000000..53a08ba8b26ef --- /dev/null +++ b/packages/sdk/src/items.ts @@ -0,0 +1,406 @@ +import { TransportRequestOptions } from './transport'; +import { ID, OptionalKeys, RequiredKeys } from './types'; + +export type Field = string; + +export type Item = Record; + +export type ItemInput = { + [P in keyof T]?: T[P] extends Record ? ItemInput : T[P]; +}; +export type InferQueryType | QueryOne> = 'data' extends keyof T ? T['data'] : T; + +export type DefaultItem = { + [K in keyof T]: NonNullable extends (infer U)[] + ? Extract, Record> extends never + ? U[] + : (string | number)[] + : Extract> extends never + ? T[K] + : Exclude> | string | number; +}; + +export type OneItem< + T extends Item, + Q extends QueryOne = Record<'fields', undefined>, + F extends string[] | false = QueryFields +> = (F extends false ? DefaultItem : PickedDefaultItem) | null | undefined; + +export type ManyItems = Record> = { + data?: NonNullable>[] | null; + meta?: ItemMetadata; +}; + +export type ItemMetadata = { + total_count?: number; + filter_count?: number; +}; + +export type Payload = Record; + +export enum Meta { + TOTAL_COUNT = 'total_count', + FILTER_COUNT = 'filter_count', +} + +export type QueryFields> = Q extends Record<'fields', unknown> + ? Q['fields'] extends string + ? [Q['fields']] + : Q['fields'] extends string[] + ? Q['fields'] + : false + : false; + +type DeepPathBranchHelper = K extends keyof V + ? TreeBranch + : K extends keyof (V & { [_ in K]: unknown }) + ? TreeBranch + : never; + +type WildCardHelper = string extends K + ? { [K: string]: T[K] } + : NonNullable extends (infer U)[] + ? Extract, Record> extends never + ? TreeLeaf + : DeepPathBranchHelper + : Extract, Record> extends never + ? TreeLeaf + : DeepPathBranchHelper; + +type DeepPathToObject< + Path extends string, + T extends Record, + Val = Record +> = string extends Path + ? never + : Path extends `${infer Key}.${infer Rest}` + ? Key extends '*' + ? Rest extends `${infer NextVal}.${string}` + ? NextVal extends '*' + ? Val & { + [K in keyof T]: WildCardHelper; + } + : Val & { + [K in keyof T]: NextVal extends keyof T[K] ? DeepPathBranchHelper : never; + } + : Rest extends '*' + ? Val & { + [K in keyof T]: WildCardHelper; + } + : Val & { + [K in keyof T]: Rest extends keyof T[K] ? DeepPathBranchHelper : never; + } + : Key extends keyof T + ? Val & { + [K in OptionalKeys>]?: DeepPathBranchHelper; + } & { + [K in RequiredKeys>]: DeepPathBranchHelper; + } + : never + : string extends keyof T + ? Val & Record + : Path extends keyof T + ? Val & { + [K in OptionalKeys>]?: TreeLeaf; + } & { + [K in RequiredKeys>]: TreeLeaf; + } + : Path extends '*' + ? Val & { + [K in OptionalKeys]?: TreeLeaf; + } & { + [K in RequiredKeys]: TreeLeaf; + } + : never; + +type TreeBranch, NT = NonNullable> = NT extends (infer U)[] + ? (ArrayTreeBranch>, Path, Val> | Exclude>)[] + : IsUnion extends true + ? DeepPathToObject>, Val> | Exclude> + : DeepPathToObject; + +type ArrayTreeBranch, NU = NonNullable> = Extract< + NU, + Record +> extends infer OB + ? Val extends (infer _)[] + ? DeepPathToObject + : DeepPathToObject + : Val extends (infer _)[] + ? DeepPathToObject + : DeepPathToObject; + +type TreeLeaf> = NT extends (infer U)[] + ? Exclude>[] + : Exclude>; + +type UnionToIntersectionFn = (TUnion extends TUnion ? (union: () => TUnion) => void : never) extends ( + intersection: infer Intersection +) => void + ? Intersection + : never; + +type LastUnion = UnionToIntersectionFn extends () => infer Last ? Last : never; + +type UnionToTuple = []> = TUnion[] extends never[] + ? TResult + : UnionToTuple>, [...TResult, LastUnion]>; + +export type PickedDefaultItem> = unknown extends T + ? any + : Fields extends string[] + ? Fields['length'] extends 0 + ? T + : UnionToTuple extends [infer First, ...infer Rest] + ? First extends string + ? IntersectionToObject< + Rest['length'] extends 0 + ? DeepPathToObject + : PickedDefaultItem> + > + : never + : never + : never; + +type IntersectionToObject = U extends (infer U2)[] + ? Array> + : U extends infer O + ? O extends string + ? string + : O extends number + ? number + : O extends symbol + ? symbol + : O extends boolean + ? boolean + : { + [K in keyof O as unknown extends O[K] ? never : K]: O[K] extends (infer U)[] + ? Array> + : IsUnion extends true + ? IntersectionToObject + : O[K] extends Record + ? IntersectionToObject + : O[K]; + } + : never; + +export type QueryOne = { + fields?: unknown extends T ? string | string[] : DotSeparated | DotSeparated[]; + search?: string; + deep?: Deep; + export?: 'json' | 'csv' | 'xml'; + filter?: Filter; +}; + +export type QueryMany = QueryOne & { + sort?: Sort; + limit?: number; + offset?: number; + page?: number; + meta?: keyof ItemMetadata | '*'; + groupBy?: string | string[]; + aggregate?: Aggregate; + alias?: Record; +}; + +export type Deep = { + [K in keyof SingleItem]?: DeepQueryMany[K]>; +}; + +export type DeepQueryMany = { + [K in keyof QueryMany> as `_${string & K}`]: QueryMany>[K]; +} & { + [K in keyof NestedObjectKeys>]?: DeepQueryMany>[K]>; +}; + +export type NestedObjectKeys = { + [P in keyof T]: NonNullable extends (infer U)[] + ? Extract> extends Record + ? Extract> + : never + : Extract, Record> extends Record + ? Extract, Record> + : never; +}; + +export type SharedAggregate = { + avg?: string[]; + avgDistinct?: string[]; + count?: string[]; + countDistinct?: string[]; + sum?: string[]; + sumDistinct?: string[]; + min?: string[]; + max?: string[]; +}; + +export type Aggregate = { + [K in keyof SharedAggregate]: string; +}; + +export type Sort = (`${Extract, string>}` | `-${Extract, string>}`)[]; + +export type FilterOperators = { + _eq?: T; + _neq?: T; + _gt?: T; + _gte?: T; + _lt?: T; + _lte?: T; + _in?: T[]; + _nin?: T[]; + _between?: [T, T]; + _nbetween?: [T, T]; + _contains?: T; + _ncontains?: T; + _starts_with?: T; + _nstarts_with?: T; + _ends_with?: T; + _nends_with?: T; + _empty?: boolean; + _nempty?: boolean; + _nnull?: boolean; + _null?: boolean; + _intersects?: T; + _nintersects?: T; + _intersects_bbox?: T; + _nintersects_bbox?: T; +}; + +export type LogicalFilterAnd = { _and: Filter[] }; +export type LogicalFilterOr = { _or: Filter[] }; +export type LogicalFilter = LogicalFilterAnd | LogicalFilterOr; + +export type FieldFilter = { + [K in keyof SingleItem]?: FilterOperators[K]> | FieldFilter[K]>; +}; + +export type Filter = LogicalFilter | FieldFilter; + +export type ItemsOptions = { + requestOptions: TransportRequestOptions; +}; + +type SingleItem = Exclude, ID>; +type Single> = NT extends Array ? NT[number] : NT; + +/** + * CRUD at its finest + */ +export interface IItems { + createOne>(item: ItemInput, query?: Q, options?: ItemsOptions): Promise>; + createMany>(items: ItemInput[], query?: Q, options?: ItemsOptions): Promise>; + + readOne>(id: ID, query?: Q, options?: ItemsOptions): Promise>; + readMany>(ids: ID[], query?: Q, options?: ItemsOptions): Promise>; + readByQuery>(query?: Q, options?: ItemsOptions): Promise>; + + updateOne>( + id: ID, + item: ItemInput, + query?: Q, + options?: ItemsOptions + ): Promise>; + updateMany>( + ids: ID[], + item: ItemInput, + query?: Q, + options?: ItemsOptions + ): Promise>; + updateBatch>( + items: ItemInput[], + query?: Q, + options?: ItemsOptions + ): Promise>; + + deleteOne(id: ID, options?: ItemsOptions): Promise; + deleteMany(ids: ID[], options?: ItemsOptions): Promise; +} + +export class EmptyParamError extends Error { + constructor(paramName?: string) { + super(`${paramName ?? 'ID'} cannot be an empty string`); + } +} + +type IsUnion = T extends unknown ? ([U] extends [T] ? false : true) : false; +type AppendToPath = Path extends '' ? Appendix : `${Path}.${Appendix}`; +type OneLevelUp = Path extends `${infer Start}.${infer Middle}.${infer Rest}` + ? Rest extends `${string}.${string}.${string}` + ? `${Start}.${Middle}.${OneLevelUp}` + : Rest extends `${infer NewMiddle}.${string}` + ? `${Start}.${Middle}.${NewMiddle}` + : Rest extends string + ? `${Start}.${Middle}` + : '' + : Path extends `${infer Start}.${string}` + ? Start + : ''; + +type LevelsToAsterisks = Path extends `${string}.${string}.${infer Rest}` + ? Rest extends `${string}.${string}.${string}` + ? `*.*.${LevelsToAsterisks}` + : Rest extends `${string}.${string}` + ? `*.*.*.*` + : Rest extends string + ? `*.*.*` + : '' + : Path extends `${string}.${string}` + ? '*.*' + : Path extends '' + ? '' + : '*'; + +type DefaultAppends< + Path extends string, + Appendix extends string, + Nested extends boolean = true, + Prepend extends boolean = true +> = + | AppendToPath + | AppendToPath, Appendix> + | (Prepend extends true + ? + | AppendToPath + | AppendToPath, Appendix> + | (OneLevelUp extends '' ? never : AppendToPath, '*'>, Appendix>) + : never) + | (Nested extends true + ? + | AppendToPath, Appendix>, '*'> + | AppendToPath, '*'>, '*'> + | AppendToPath, '*'> + | AppendToPath, '*'> + | (OneLevelUp extends '' + ? never + : AppendToPath, '*'>, Appendix>, '*'>) + : never); + +type DotSeparated< + T, + N extends number, + Level extends number[] = [], + Path extends string = '' +> = Level['length'] extends N + ? Path + : T extends (infer U)[] + ? Extract> extends Record + ? DotSeparated>, N, Level, Path> + : Path + : Extract, Record> extends Record + ? { + [K in keyof T]: K extends string + ? NonNullable extends (infer U)[] + ? Extract> extends never + ? DefaultAppends + : + | DotSeparated>, N, [...Level, 0], AppendToPath> + | DefaultAppends + : Extract> extends never + ? DefaultAppends + : + | DotSeparated>, N, [...Level, 0], AppendToPath> + | DefaultAppends + : never; + }[keyof T] + : never; diff --git a/packages/sdk/src/singleton.ts b/packages/sdk/src/singleton.ts new file mode 100644 index 0000000000000..214d18dcb4232 --- /dev/null +++ b/packages/sdk/src/singleton.ts @@ -0,0 +1,9 @@ +import { Item, OneItem, ItemInput, QueryOne } from './items'; + +/** + * CRUD at its finest + */ +export interface ISingleton { + read>(query?: Q): Promise>; + update>(item: ItemInput, query?: Q): Promise>; +} diff --git a/packages/sdk/src/storage.ts b/packages/sdk/src/storage.ts new file mode 100644 index 0000000000000..c9c1d63d50fcc --- /dev/null +++ b/packages/sdk/src/storage.ts @@ -0,0 +1,10 @@ +export abstract class IStorage { + abstract auth_token: string | null; + abstract auth_expires: number | null; + abstract auth_expires_at: number | null; + abstract auth_refresh_token: string | null; + + abstract get(key: string): string | null; + abstract set(key: string, value: string): string; + abstract delete(key: string): string | null; +} diff --git a/packages/sdk/src/transport.ts b/packages/sdk/src/transport.ts new file mode 100644 index 0000000000000..fc05fd04f6090 --- /dev/null +++ b/packages/sdk/src/transport.ts @@ -0,0 +1,88 @@ +import { AxiosRequestConfig, ResponseType } from 'axios'; +import { ItemMetadata } from './items'; + +export type TransportErrorDescription = { + message?: string; + extensions?: Record & { + code?: string; + }; +}; + +export type TransportResponse = { + raw: R; + data?: T; + meta?: ItemMetadata; + errors?: TransportErrorDescription[]; + status: number; + statusText?: string; + headers: any; +}; + +export type TransportMethods = 'get' | 'delete' | 'head' | 'options' | 'post' | 'put' | 'patch'; + +export type TransportRequestOptions = { + params?: any; + headers?: any; + responseType?: ResponseType; + onUploadProgress?: ((progressEvent: any) => void) | undefined; + maxBodyLength?: number; + maxContentLength?: number; +}; + +export type TransportOptions = TransportRequestOptions & { + url: string; + beforeRequest?: (config: AxiosRequestConfig) => Promise; +}; + +export abstract class ITransport { + abstract get(path: string, options?: TransportRequestOptions): Promise>; + abstract head(path: string, options?: TransportRequestOptions): Promise>; + abstract options(path: string, options?: TransportRequestOptions): Promise>; + abstract delete( + path: string, + data?: P, + options?: TransportRequestOptions + ): Promise>; + + abstract post( + path: string, + data?: P, + options?: TransportRequestOptions + ): Promise>; + + abstract put( + path: string, + data?: P, + options?: TransportRequestOptions + ): Promise>; + + abstract patch( + path: string, + data?: P, + options?: TransportRequestOptions + ): Promise>; +} + +export class TransportError extends Error { + public readonly errors: TransportErrorDescription[]; + public readonly response?: Partial>; + public readonly parent: Error | null; + + constructor(parent: Error | null, response?: Partial>) { + if (response?.errors?.length) { + super(response?.errors[0]?.message); + } else { + super(parent?.message || 'Unknown transport error'); + } + + this.parent = parent; + this.response = response; + this.errors = response?.errors || []; + + if (!Object.values(response || {}).some((value) => value !== undefined)) { + this.response = undefined; + } + + Object.setPrototypeOf(this, TransportError.prototype); + } +} diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts new file mode 100644 index 0000000000000..aaba13d4931da --- /dev/null +++ b/packages/sdk/src/types.ts @@ -0,0 +1,363 @@ +export type ID = number | string; + +export type DefaultType = { + [field: string]: any; +}; + +export type SystemType = DefaultType & T; + +export type TypeMap = { + [k: string]: unknown; +}; + +export type TypeOf = T[K] extends undefined ? DefaultType : T[K]; + +export type Omit = Pick>; + +export type PartialBy = Omit & Partial>; + +export type ActivityType = SystemType<{ + // TODO: review + action: string; + collection: string; + comment: string | null; + id: number; + ip: string; + item: string; + origin: string | null; + timestamp: string; + revisions: number[]; + user: string; + user_agent: string; +}>; + +export type Comment = SystemType<{ + // TODO: review + collection: string; + comment: string; + item: string; +}>; + +export type CollectionType = SystemType<{ + // TODO: review + collection: string; + meta: CollectionMetaType; + schema: CollectionSchemaType | null; +}>; + +export type CollectionMetaType = SystemType<{ + // TODO: review + accountability: string | null; + archive_app_filter: boolean; + archive_field: string | null; + archive_value: string | null; + collapse: string; + collection: string; + display_template: string | null; + group: string | null; + hidden: boolean; + icon: string | null; + item_duplication_fields: string[] | null; + note: string | null; + singleton: boolean; + sort_field: string | null; + translations: CollectionMetaTranslationType[] | null; + unarchive_value: string | null; +}>; + +export type CollectionMetaTranslationType = SystemType<{ + // TODO: review + language: string; + plural: string; + singular: string; + translation: string; +}>; + +export type CollectionSchemaType = SystemType<{ + // TODO: review + comment: string | null; + name: string; + schema: string; +}>; + +export type FieldType = SystemType<{ + // TODO: complete + collection: string; + field: string; + meta: FieldMetaType; + schema: FieldSchemaType; + type: string; +}>; + +export type FieldMetaType = SystemType<{ + // TODO: review + collection: string; + conditions: FieldMetaConditionType[] | null; + display: string | null; + display_options: string | null; + field: string; + group: string | null; + hidden: boolean; + id: number; + interface: string; + note: string | null; + // TODO: options vary by field type + options: DefaultType | null; + readonly: boolean; + required: boolean; + sort: number | null; + special: string[] | null; + translations: FieldMetaTranslationType[] | null; + validation: DefaultType | null; + validation_message: string | null; + width: string; +}>; + +export type FieldMetaConditionType = SystemType<{ + // TODO: review + hidden: boolean; + name: string; + options: FieldMetaConditionOptionType; + readonly: boolean; + required: boolean; + // TODO: rules use atomic operators and can nest + rule: DefaultType; +}>; + +export type FieldMetaTranslationType = SystemType<{ + // TODO: review + language: string; + translation: string; +}>; + +export type FieldMetaConditionOptionType = SystemType<{ + // TODO: review + clear: boolean; + font: string; + iconLeft?: string; + iconRight?: string; + masked: boolean; + placeholder: string; + slug: boolean; + softLength?: number; + trim: boolean; +}>; + +export type FieldSchemaType = SystemType<{ + // TODO: review + comment: string | null; + data_type: string; + default_value: any | null; + foreign_key_column: string | null; + foreign_key_schema: string | null; + foreign_key_table: string | null; + generation_expression: unknown | null; + has_auto_increment: boolean; + is_generated: boolean; + is_nullable: boolean; + is_primary_key: boolean; + is_unique: boolean; + max_length: number | null; + name: string; + numeric_precision: number | null; + numeric_scale: number | null; + schema: string; + table: string; +}>; + +export type FileType = SystemType<{ + // TODO: review + charset: string | null; + description: string | null; + duration: number | null; + embed: unknown | null; + filename_disk: string; + filename_download: string; + filesize: string; + folder: string; + height: number | null; + id: string; + location: string | null; + // TODO: is it possible to determine all possible metadata? + metadata: DefaultType; + modified_by: string; + modified_on: string; + storage: string; + tags: string[]; + title: string; + type: string; + uploaded_by: string; + uploaded_on: string; + width: number | null; +}>; + +export type FolderType = SystemType<{ + // TODO: review + id: string; + name: string; + parent: string; +}>; + +export type PermissionType = SystemType<{ + // TODO: review + action: string; + collection: string | null; + fields: string[]; + id: string; + // TODO: object will vary by schema + permissions: DefaultType; + // TODO: object will vary by schema + presets: DefaultType | null; + role: string | null; + system?: boolean; + // TODO: object will vary by schema + validation: DefaultType | null; +}>; + +export type PresetType = SystemType<{ + // TODO: review + collection: string; + color: string | null; + bookmark: string | null; + // TODO: rules use atomic operators and can nest + filter: DefaultType; + icon: string | null; + id: number; + layout: string | null; + // TODO: determine possible properties + layout_options: DefaultType; + // TODO: determine possible properties + layout_query: DefaultType; + refresh_interval: number | null; + role: string | null; + search: string | null; + user: string | null; +}>; + +export type RelationType = SystemType<{ + // TODO: review + collection: string; + field: string; + related_collection: string; + schema: RelationSchemaType; + meta: RelationMetaType; +}>; + +export type RelationMetaType = SystemType<{ + // TODO: review + id: number | null; + junction_field: string | null; + many_collection: string | null; + many_field: string | null; + one_allowed_collections: string | null; + one_collection: string | null; + one_collection_field: string | null; + one_deselect_action: string; + one_field: string | null; + sort_field: string | null; + system: boolean | null; +}>; + +export type RelationSchemaType = SystemType<{ + // TODO: review + column: string; + constraint_name: string; + foreign_key_column: string; + foreign_key_schema: string; + foreign_key_table: string; + on_delete: string; + on_update: string; + table: string; +}>; + +export type RevisionType = SystemType<{ + // TODO: review + activity: number; + collection: string; + // TODO: object will vary by schema + data: DefaultType; + // TODO: object will vary by schema + delta: DefaultType; + id: number; + item: string; + parent: number | null; +}>; + +export type RoleType = SystemType<{ + // TODO: review + admin_access: boolean; + app_access: boolean; + description: string | null; + enforce_tfa: boolean; + icon: string; + id: string; + ip_access: string[] | null; + name: string; + users: string[]; +}>; + +export type SettingType = SystemType<{ + // TODO: review + id: 1; + auth_login_attempts: number; + auth_password_policy: string | null; + custom_css: string | null; + project_color: string | null; + project_logo: string | null; + project_name: string; + project_url: string; + public_background: string | null; + public_foreground: string | null; + public_note: string | null; + storage_asset_presets: + | { + fit: string; + height: number; + width: number; + quality: number; + key: string; + withoutEnlargement: boolean; + }[] + | null; + storage_asset_transform: 'all' | 'none' | 'presets'; +}>; + +export type UserType = SystemType<{ + // TODO: review + // TODO: determine possible properties + auth_data: DefaultType; + avatar: string; + description: string | null; + email: string | null; + email_notifications: boolean; + external_identifier: string; + first_name: string | null; + id: string; + language: string | null; + last_access: string | null; + last_name: string | null; + last_page: string | null; + location: string | null; + password: string | null; // will just be *s + provider: string; + role: string; + status: string; + tags: string[]; + theme: string; + tfa_secret: string | null; + title: string | null; + token: string | null; +}>; + +export type TfaType = SystemType<{ + secret: string; + otpauth_url: string; +}>; + +export type RequiredKeys = { + [K in keyof T]-?: Record extends { [P in K]: T[K] } ? never : K; +}[keyof T]; + +export type OptionalKeys = { + [K in keyof T]-?: Record extends { [P in K]: T[K] } ? K : never; +}[keyof T]; diff --git a/packages/sdk/tests/base/auth.browser.test.ts b/packages/sdk/tests/base/auth.browser.test.ts new file mode 100644 index 0000000000000..2df5af7ca24cd --- /dev/null +++ b/packages/sdk/tests/base/auth.browser.test.ts @@ -0,0 +1,85 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { rest } from 'msw'; +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; + +describe('auth (browser)', function () { + beforeEach(() => { + localStorage.clear(); + }); + + afterEach(() => { + localStorage.clear(); + }); + + it(`sets default auth mode to cookie`, async () => { + const sdk = new Directus(URL, { auth: { mode: 'cookie' } }); + expect(sdk.auth.mode).toBe('cookie'); + }); + + it(`sends default auth mode`, async () => { + mockServer.use( + rest.post(URL + '/auth/login', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + access_token: 'access_token', + refresh_token: 'refresh_token', + expires: 60000, + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + + await expect( + sdk.auth.login({ + email: 'wolfulus@gmail.com', + password: 'password', + }) + ).resolves.not.toThrowError(); + + // expect(scope.pendingMocks().length).toBe(0); + }); + + it(`logout doesn't send a refresh token due to cookie mode`, async () => { + mockServer.use( + rest.post(URL + '/auth/login', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + access_token: 'some_access_token', + expires: 60000, + }, + }), + ctx.cookie('directus_refresh_token', 'my_refresh_token', { httpOnly: true }) + ) + ), + rest.post(URL + '/auth/logout', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: {}, + }) + ) + ) + ); + + const sdk = new Directus(URL); + + await sdk.auth.login({ + email: 'wolfulus@gmail.com', + password: 'password', + }); + + expect(await sdk.auth.token).toBe('some_access_token'); + + await sdk.auth.logout(); + + expect(await sdk.auth.token).toBeNull(); + }); +}); diff --git a/packages/sdk/tests/base/auth.node.test.ts b/packages/sdk/tests/base/auth.node.test.ts new file mode 100644 index 0000000000000..82c2fd8ca1d0e --- /dev/null +++ b/packages/sdk/tests/base/auth.node.test.ts @@ -0,0 +1,74 @@ +// @vitest-environment node +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { rest } from 'msw'; +import { describe, expect, it } from 'vitest'; + +describe('auth (node)', function () { + it(`sets default auth mode to json`, async () => { + const sdk = new Directus(URL); + expect(sdk.auth.mode).toBe('json'); + }); + + it(`sends default auth mode`, async () => { + mockServer.use( + rest.post(URL + '/auth/login', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + access_token: 'access_token', + refresh_token: 'refresh_token', + expires: 60000, + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const loginPromise = sdk.auth.login({ + email: 'wolfulus@gmail.com', + password: 'password', + }); + + await expect(loginPromise).resolves.not.toThrowError(); + }); + + it(`logout sends a refresh token in body`, async () => { + mockServer.use( + rest.post(URL + '/auth/login', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + access_token: 'auth_token', + refresh_token: 'json_refresh_token', + expires: 60000, + }, + }), + ctx.cookie('directus_refresh_token', 'my_refresh_token', { httpOnly: true }) + ) + ), + rest.post(URL + '/auth/logout', (_req, res, ctx) => res(ctx.status(200), ctx.json({ data: {} }))) + ); + + const sdk = new Directus(URL); + + const loginPromise = sdk.auth.login({ + email: 'wolfulus@gmail.com', + password: 'password', + }); + + await loginPromise; + + expect(await sdk.auth.token).toBe('auth_token'); + + const logoutPromise = sdk.auth.logout(); + + await logoutPromise; + + expect(await sdk.auth.token).toBeNull(); + }); +}); diff --git a/packages/sdk/tests/base/auth.test.ts b/packages/sdk/tests/base/auth.test.ts new file mode 100644 index 0000000000000..35b736db07f36 --- /dev/null +++ b/packages/sdk/tests/base/auth.test.ts @@ -0,0 +1,174 @@ +import { Auth, Directus, MemoryStorage, Transport } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('auth', function () { + it(`static auth should validate token`, async () => { + mockServer.use(rest.get(URL + '/users/me', (_req, res, ctx) => res(ctx.status(203)))); + + const sdk = new Directus(URL); + await sdk.auth.static('token'); + }); + + it(`should allow to extend auth handler`, async () => { + class CustomAuthHandler extends Auth { + hello() { + return 'hello'; + } + } + + // eslint-disable-next-line prefer-const + let auth: CustomAuthHandler; + const storage = new MemoryStorage(); + + const transport = new Transport({ + url: URL, + beforeRequest: async (config) => { + await auth.refreshIfExpired(); + const token = auth.storage.auth_token; + + let bearer = ''; + + if (token) { + bearer = token.startsWith(`Bearer `) ? String(auth.storage.auth_token) : `Bearer ${auth.storage.auth_token}`; + } + + return { + ...config, + headers: { + Authorization: bearer, + ...config.headers, + }, + }; + }, + }); + + auth = new CustomAuthHandler({ storage, transport }); + + const sdk = new Directus(URL, { + auth, + storage: auth.storage, + transport: auth.transport, + }); + + mockServer.use(rest.get(URL + '/users/me', (_req, res, ctx) => res(ctx.status(203)))); + + await sdk.auth.static('token'); + expect(sdk.auth.hello()).toBe('hello'); + }); + + /* + it(`throws when refresh time resolves to an invalid value`, async () => { + nock() + .post('/auth/login') + .reply(200, { + data: { + access_token: 'access_token', + refresh_token: 'refresh_token', + expires: 10000, // expires in 10 seconds + }, + }); + + const sdk = new Directus(URL); + try { + await sdk.auth.login( + { + email: 'wolfulus@gmail.com', + password: 'password', + }, + { + refresh: { + auto: true, + time: 15000, // but we ask to refresh 15 seconds before the expiration + }, + } + ); + fail('Should have failed'); + } catch (err: any) { + expect(err).toBeInstanceOf(InvalidRefreshTime); + } + }); + */ + + it(`successful auth should set the token`, async () => { + mockServer.use(rest.get(URL + '/users/me', (_req, res, ctx) => res(ctx.status(203)))); + + const sdk = new Directus(URL); + await sdk.auth.static('token'); + + expect(await sdk.auth.token); + }); + + it(`invalid credentials token should not set the token`, async () => { + mockServer.use( + rest.get(URL + '/users/me', (_req, res, ctx) => + res( + ctx.status(401), + ctx.json({ + errors: [ + { + message: 'Invalid token', + extensions: { + code: 'EUNAUTHORIZED', + }, + }, + ], + }) + ) + ) + ); + + const sdk = new Directus(URL); + + let failed: false | string = false; + + try { + await sdk.auth.login({ + email: 'invalid@email.com', + password: 'invalid_password', + }); + + failed = 'Should have thrown due to error response'; + } catch { + // + } + + expect(await sdk.auth.token).toBeNull(); + expect(failed).toBe(false); + }); + + it(`invalid static token should not set the token`, async () => { + mockServer.use( + rest.get(URL + '/users/me', (_req, res, ctx) => + res( + ctx.status(401), + ctx.json({ + errors: [ + { + message: 'Invalid token', + extensions: { + code: 'EUNAUTHORIZED', + }, + }, + ], + }) + ) + ) + ); + + const sdk = new Directus(URL); + + let failed: false | string = false; + + try { + await sdk.auth.static('token'); + failed = 'Should have thrown due to error response'; + } catch { + // + } + + expect(await sdk.auth.token).toBeNull(); + expect(failed).toBe(false); + }); +}); diff --git a/packages/sdk/tests/base/directus.browser.test.ts b/packages/sdk/tests/base/directus.browser.test.ts new file mode 100644 index 0000000000000..72dbd68eefd25 --- /dev/null +++ b/packages/sdk/tests/base/directus.browser.test.ts @@ -0,0 +1,14 @@ +import { Directus, LocalStorage, MemoryStorage } from '../../src/base'; +import { describe, expect, it } from 'vitest'; + +describe('browser sdk', function () { + it('has storage', function () { + const sdk = new Directus('http://example.com'); + expect(sdk.storage).toBeInstanceOf(LocalStorage); + }); + + it('has memory storage', function () { + const sdk = new Directus('http://example.com', { storage: { mode: 'MemoryStorage' } }); + expect(sdk.storage).toBeInstanceOf(MemoryStorage); + }); +}); diff --git a/packages/sdk/tests/base/directus.node.ts b/packages/sdk/tests/base/directus.node.ts new file mode 100644 index 0000000000000..4305c85533070 --- /dev/null +++ b/packages/sdk/tests/base/directus.node.ts @@ -0,0 +1,11 @@ +// @vitest-environment node +import { Directus, MemoryStorage } from '../../src/base'; +import { describe, expect, it } from 'vitest'; + +describe('node sdk', function () { + const sdk = new Directus('http://example.com'); + + it('has storage', function () { + expect(sdk.storage).toBeInstanceOf(MemoryStorage); + }); +}); diff --git a/packages/sdk/tests/base/directus.test.ts b/packages/sdk/tests/base/directus.test.ts new file mode 100644 index 0000000000000..ca07d2fd65d1e --- /dev/null +++ b/packages/sdk/tests/base/directus.test.ts @@ -0,0 +1,188 @@ +import { Auth } from '../../src/base/auth'; +import { ItemsHandler } from '../../src/base/items'; +import { Transport } from '../../src/base/transport'; +import { Directus } from '../../src/base'; +import { + ActivityHandler, + CollectionsHandler, + CommentsHandler, + FieldsHandler, + FilesHandler, + FoldersHandler, + PermissionsHandler, + PresetsHandler, + RelationsHandler, + RevisionsHandler, + RolesHandler, + ServerHandler, + SettingsHandler, + UsersHandler, + UtilsHandler, +} from '../../src/handlers'; +import { mockServer, URL } from '../utils'; +import { InvitesHandler } from '../../src/handlers/invites'; +import { TFAHandler } from '../../src/handlers/tfa'; +import { MeHandler } from '../../src/handlers/me'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('sdk', function () { + const sdk = new Directus(URL); + + it('has auth', function () { + expect(sdk.auth).toBeInstanceOf(Auth); + }); + + it('has transport', function () { + expect(sdk.transport).toBeInstanceOf(Transport); + }); + + it('has activity instance', function () { + expect(sdk.activity).toBeInstanceOf(ActivityHandler); + }); + + it('has activity instance', function () { + expect(sdk.activity.comments).toBeInstanceOf(CommentsHandler); + }); + + it('has collections instance', function () { + expect(sdk.collections).toBeInstanceOf(CollectionsHandler); + }); + + it('has fields instance', function () { + expect(sdk.fields).toBeInstanceOf(FieldsHandler); + }); + + it('has files instance', function () { + expect(sdk.files).toBeInstanceOf(FilesHandler); + }); + + it('has folders instance', function () { + expect(sdk.folders).toBeInstanceOf(FoldersHandler); + }); + + it('has permissions instance', function () { + expect(sdk.permissions).toBeInstanceOf(PermissionsHandler); + }); + + it('has presets instance', function () { + expect(sdk.presets).toBeInstanceOf(PresetsHandler); + }); + + it('has relations instance', function () { + expect(sdk.relations).toBeInstanceOf(RelationsHandler); + }); + + it('has revisions instance', function () { + expect(sdk.revisions).toBeInstanceOf(RevisionsHandler); + }); + + it('has roles instance', function () { + expect(sdk.roles).toBeInstanceOf(RolesHandler); + }); + + it('has server instance', function () { + expect(sdk.server).toBeInstanceOf(ServerHandler); + }); + + it('has settings instance', function () { + expect(sdk.settings).toBeInstanceOf(SettingsHandler); + }); + + it('has users instance', function () { + expect(sdk.users).toBeInstanceOf(UsersHandler); + }); + + it('has users invites', function () { + expect(sdk.users.invites).toBeInstanceOf(InvitesHandler); + }); + + it('has user profile', function () { + expect(sdk.users.me).toBeInstanceOf(MeHandler); + }); + + it('has users tfa', function () { + expect(sdk.users.me.tfa).toBeInstanceOf(TFAHandler); + }); + + it('has utils instance', function () { + expect(sdk.utils).toBeInstanceOf(UtilsHandler); + }); + + it('has items', async function () { + expect(sdk.items('collection')).toBeInstanceOf(ItemsHandler); + }); + + it('can run graphql', async () => { + mockServer.use( + rest.post(URL + '/graphql', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + posts: [ + { id: 1, title: 'My first post' }, + { id: 2, title: 'My second post' }, + ], + }, + }) + ) + ) + ); + + const query = ` + query { + posts { + id + title + } + } + `; + + const sdk = new Directus(URL); + + const response = await sdk.graphql.items(query); + + expect(response.data).toMatchObject({ + posts: [ + { id: 1, title: 'My first post' }, + { id: 2, title: 'My second post' }, + ], + }); + + // expect(scope.pendingMocks().length).toBe(0); + }); + + it('can run graphql on system', async () => { + mockServer.use( + rest.post(URL + '/graphql/system', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + users: [{ email: 'someone@example.com' }, { email: 'someone.else@example.com' }], + }, + }) + ) + ) + ); + + const query = ` + query { + users { + email + } + } + `; + + const sdk = new Directus(URL); + + const response = await sdk.graphql.system(query); + + expect(response.data).toMatchObject({ + users: [{ email: 'someone@example.com' }, { email: 'someone.else@example.com' }], + }); + + // expect(scope.pendingMocks().length).toBe(0); + }); +}); diff --git a/packages/sdk/tests/base/storage/localstorage.test.ts b/packages/sdk/tests/base/storage/localstorage.test.ts new file mode 100644 index 0000000000000..dafb67b2c14dc --- /dev/null +++ b/packages/sdk/tests/base/storage/localstorage.test.ts @@ -0,0 +1,8 @@ +import { LocalStorage } from '../../../src/base/storage'; +import { createStorageTests } from './tests'; +import { describe } from 'vitest'; + +describe( + 'localstorage storage', + createStorageTests(() => new LocalStorage()) +); diff --git a/packages/sdk/tests/base/storage/memory.test.ts b/packages/sdk/tests/base/storage/memory.test.ts new file mode 100644 index 0000000000000..ccb2534ffbb71 --- /dev/null +++ b/packages/sdk/tests/base/storage/memory.test.ts @@ -0,0 +1,8 @@ +import { MemoryStorage } from '../../../src/base/storage'; +import { createStorageTests } from './tests'; +import { describe } from 'vitest'; + +describe( + 'memory storage', + createStorageTests(() => new MemoryStorage()) +); diff --git a/packages/sdk/tests/base/storage/tests.ts b/packages/sdk/tests/base/storage/tests.ts new file mode 100644 index 0000000000000..f6ad7c45a2312 --- /dev/null +++ b/packages/sdk/tests/base/storage/tests.ts @@ -0,0 +1,90 @@ +import { IStorage } from '../../../src/storage'; +import { beforeEach, test, expect } from 'vitest'; + +export function createStorageTests(createStorage: () => IStorage) { + return function (): void { + beforeEach(() => { + // These run both in node and browser mode + if (typeof localStorage !== 'undefined') { + localStorage.clear(); + } + }); + + test('set returns the same value', async function () { + const storage = createStorage(); + + const value = storage.set('value', '1234'); + expect(value).toBe('1234'); + }); + + test('get returns previously set items', async function () { + const storage = createStorage(); + + storage.set('value1', '1234'); + const value1 = storage.get('value1'); + expect(value1).toBe('1234'); + + storage.set('value2', 'string'); + const value2 = storage.get('value2'); + expect(value2).toBe('string'); + + storage.set('value3', 'false'); + const value3 = storage.get('value3'); + expect(value3).toBe('false'); + }); + + test('get returns null for missing items', async function () { + const storage = createStorage(); + + const value = storage.get('value'); + expect(value).toBeNull(); + }); + + test('delete removes items and returns it', async function () { + const storage = createStorage(); + + let value = storage.get('abobrinha'); + expect(value).toBeNull(); + + value = storage.set('value', '12345'); + expect(value).toBe('12345'); + + value = storage.delete('value'); + expect(value).toBe('12345'); + + value = storage.get('value'); + expect(value).toBeNull(); + }); + + test('delete returns null if removing an unknown key', async function () { + const storage = createStorage(); + + const value = storage.delete('unknown'); + expect(value).toBeNull(); + }); + + test('can get and set auth_token', async function () { + const storage = createStorage(); + + expect(storage.auth_token).toBeNull(); + + storage.auth_token = '12345'; + expect(storage.auth_token).toBe('12345'); + + storage.auth_token = null; + expect(storage.auth_token).toBeNull(); + }); + + test('can get and set auth_expires', async function () { + const storage = createStorage(); + + expect(storage.auth_expires).toBeNull(); + + storage.auth_expires = 12345; + expect(storage.auth_expires).toBe(12345); + + storage.auth_expires = null; + expect(storage.auth_expires).toBeNull(); + }); + }; +} diff --git a/packages/sdk/tests/base/transport.test.ts b/packages/sdk/tests/base/transport.test.ts new file mode 100644 index 0000000000000..7a3855abda942 --- /dev/null +++ b/packages/sdk/tests/base/transport.test.ts @@ -0,0 +1,152 @@ +import { Transport } from '../../src/base/transport'; +import { TransportResponse, TransportError } from '../../src/transport'; +import { describe, expect, it, vi } from 'vitest'; +import { mockServer, URL } from '../utils'; +import { rest } from 'msw'; + +describe('default transport', function () { + function expectResponse(response: TransportResponse, expected: Partial>) { + if (expected.status) { + expect(response.status).toBe(expected.status); + } + + if (expected.statusText) { + expect(response.statusText).toBe(expected.statusText); + } + + if (expected.data) { + expect(response.data).toMatchObject(expected.data); + } + + if (expected.headers) { + expect(response.headers).toMatchObject(expected.headers); + } + } + + ['get', 'delete', 'head', 'options', 'put', 'post', 'patch'].forEach((method) => { + it(`${method} should return a response object`, async () => { + const route = `/${method}/response`; + + mockServer.use(rest[method](URL + route, (_req, res, ctx) => res(ctx.status(200)))); + + const transport = new Transport({ url: URL }) as any; + const response = await transport[method](route); + + expectResponse(response, { + status: 200, + }); + }); + + it(`${method} should throw on response errors`, async function () { + const route = `/${method}/500`; + mockServer.use(rest[method](URL + route, (_req, res, ctx) => res(ctx.status(500)))); + + const transport = new Transport({ url: URL }) as any; + + let failed = false; + + try { + await transport[method](route); + failed = true; + } catch (err: any) { + expect(err).toBeInstanceOf(TransportError); + } + + expect(failed).toBe(false); + }); + + it(`${method} should carry response error information`, async function () { + const route = `/${method}/403/error`; + + mockServer.use( + rest[method](URL + route, (_req, res, ctx) => + res( + ctx.status(403), + ctx.json({ + errors: [ + { + message: 'You don\'t have permission access to "contacts" collection.', + extensions: { + code: 'FORBIDDEN', + }, + }, + ], + }) + ) + ) + ); + + const transport = new Transport({ url: URL }) as any; + + let failed = false; + + try { + await transport[method](route); + failed = true; + } catch (err: any) { + const terr = err as TransportError; + expect(terr).toBeInstanceOf(TransportError); + expect(terr.response?.status).toBe(403); + expect(terr.message).toBe('You don\'t have permission access to "contacts" collection.'); + expect(terr.errors.length).toBe(1); + expect(terr.errors[0]?.message).toBe('You don\'t have permission access to "contacts" collection.'); + expect(terr.errors[0]?.extensions?.code).toBe('FORBIDDEN'); + } + + expect(failed).toBe(false); + }); + + // I am unsure how to mock this with msw + // it('get should throw non response errors', async function () { + // const route = `/${method}/this/raises/error`; + // mockServer.use(rest[method](URL + route, (_req, res, ctx) => res(ctx.status(500)))); + + // const transport = new Transport({ url: URL }) as any; + // let failed = false; + + // try { + // await transport[method](route); + // failed = true; + // } catch (err: any) { + // const terr = err as TransportError; + // expect(terr).toBeInstanceOf(TransportError); + // expect(terr.response).toBeUndefined(); + // expect(terr.message).toBe('Random error'); + // expect(terr.parent).not.toBeUndefined(); + // expect(terr.parent?.message).toBe('Random error'); + // } + + // expect(failed).toBe(false); + // }); + }); + + it('returns the configured url', async function () { + const transport = new Transport({ url: URL }); + expect(transport.url).toBe(URL); + }); + + it('non axios errors are set in parent', async function () { + const transport = new Transport({ url: URL }); + const mock = vi.spyOn(transport, 'beforeRequest'); + + mock.mockImplementation(() => { + throw new Error('this is not an axios error'); + }); + + let failed = false; + + try { + await transport.get('/route'); + failed = true; + } catch (err: any) { + const terr = err as TransportError; + expect(terr).toBeInstanceOf(TransportError); + expect(terr.response).toBeUndefined(); + expect(terr.message).toBe('this is not an axios error'); + expect(terr.parent).not.toBeUndefined(); + expect(terr.parent?.message).toBe('this is not an axios error'); + } + + expect(failed).toBe(false); + }); +}); diff --git a/packages/sdk/tests/blog.d.ts b/packages/sdk/tests/blog.d.ts new file mode 100644 index 0000000000000..9c611fa9765d1 --- /dev/null +++ b/packages/sdk/tests/blog.d.ts @@ -0,0 +1,26 @@ +import { ID } from '../src/types'; + +export type Post = { + id: ID; + title: string; + body: string; + published: boolean; + author: ID | Author; +}; + +export type Category = { + slug: string; + name: string; +}; + +export type Author = { + id: ID; + name: string; + posts: (ID | Post)[]; +}; + +export type Blog = { + posts: Post; + categories: Category; + author: Author; +}; diff --git a/packages/sdk/tests/handlers/comments.test.ts b/packages/sdk/tests/handlers/comments.test.ts new file mode 100644 index 0000000000000..cb4606ede4e74 --- /dev/null +++ b/packages/sdk/tests/handlers/comments.test.ts @@ -0,0 +1,65 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('comments', function () { + it(`creates comments`, async () => { + mockServer.use( + rest.post(URL + '/activity/comment', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + id: 5, + collection: 'posts', + item: '1', + comment: 'Awesome post!', + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const item = await sdk.activity.comments.create({ + collection: 'posts', + item: '1', + comment: 'Awesome post!', + }); + + expect(item.id).toBe(5); + expect(item.comment).toBe('Awesome post!'); + }); + + it(`updates comments`, async () => { + mockServer.use( + rest.patch(URL + '/activity/comment/5', (_req, res, ctx) => + res( + ctx.status(202), + ctx.json({ + data: { + id: 5, + collection: 'posts', + item: '1', + comment: 'Awesome content!', + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + const item = await sdk.activity.comments.update(5, 'Awesome content!'); + + expect(item.comment).toBe('Awesome content!'); + }); + + it(`deletes comments`, async () => { + mockServer.use(rest.delete(URL + '/activity/comment/5', (_req, res, ctx) => res(ctx.status(204)))); + + const sdk = new Directus(URL); + await expect(sdk.activity.comments.delete(5)).resolves.not.toThrowError(); + }); +}); diff --git a/packages/sdk/tests/handlers/fields.test.ts b/packages/sdk/tests/handlers/fields.test.ts new file mode 100644 index 0000000000000..e18e00d03d4a4 --- /dev/null +++ b/packages/sdk/tests/handlers/fields.test.ts @@ -0,0 +1,56 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('fields', function () { + it(`update one`, async () => { + mockServer.use(rest.patch(URL + '/fields/posts/title', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + + await expect( + sdk.fields.updateOne('posts', 'title', { + meta: { + required: true, + }, + }) + ).resolves.not.toThrowError(); + }); + + it(`check ManyItems return type for readAll`, async () => { + mockServer.use( + rest.get(URL + '/fields', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: [], + }) + ) + ) + ); + + const sdk = new Directus(URL); + const response = await sdk.fields.readAll(); + + expect(Array.isArray(response.data)).toBe(true); + }); + + it(`check ManyItems return type for readMany`, async () => { + mockServer.use( + rest.get(URL + '/fields/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: [], + }) + ) + ) + ); + + const sdk = new Directus(URL); + const response = await sdk.fields.readMany('posts'); + + expect(Array.isArray(response.data)).toBe(true); + }); +}); diff --git a/packages/sdk/tests/handlers/invites.test.ts b/packages/sdk/tests/handlers/invites.test.ts new file mode 100644 index 0000000000000..5ff30319dcdc3 --- /dev/null +++ b/packages/sdk/tests/handlers/invites.test.ts @@ -0,0 +1,23 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('invites', function () { + it('send', async () => { + mockServer.use(rest.post(URL + '/users/invite', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + + await expect( + sdk.users.invites.send('admin@example.com', '1e098175-6258-48d6-ad88-d24cae2abe15') + ).resolves.not.toThrowError(); + }); + + it(`accept`, async () => { + mockServer.use(rest.post(URL + '/users/invite/accept', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + await expect(sdk.users.invites.accept('token', 'password1234')).resolves.not.toThrowError(); + }); +}); diff --git a/packages/sdk/tests/handlers/me.test.ts b/packages/sdk/tests/handlers/me.test.ts new file mode 100644 index 0000000000000..d5e049af349f6 --- /dev/null +++ b/packages/sdk/tests/handlers/me.test.ts @@ -0,0 +1,26 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('profile', function () { + it(`read`, async () => { + mockServer.use(rest.get(URL + '/users/me', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + await expect(sdk.users.me.read()).resolves.not.toThrowError(); + }); + + it(`update`, async () => { + mockServer.use(rest.patch(URL + '/users/me', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + + await expect( + sdk.users.me.update({ + email: 'other@email.com', + untyped_field: 12345, + }) + ).resolves.not.toThrowError(); + }); +}); diff --git a/packages/sdk/tests/handlers/passwords.test.ts b/packages/sdk/tests/handlers/passwords.test.ts new file mode 100644 index 0000000000000..444fad9a78f04 --- /dev/null +++ b/packages/sdk/tests/handlers/passwords.test.ts @@ -0,0 +1,20 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('password', function () { + it(`request`, async () => { + mockServer.use(rest.post(URL + '/auth/password/request', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + await expect(sdk.auth.password.request('admin@example.com', 'http://some_url.com')).resolves.not.toThrowError(); + }); + + it(`reset`, async () => { + mockServer.use(rest.post(URL + '/auth/password/reset', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + await expect(sdk.auth.password.reset('token', 'newpassword')).resolves.not.toThrowError(); + }); +}); diff --git a/packages/sdk/tests/handlers/relations.test.ts b/packages/sdk/tests/handlers/relations.test.ts new file mode 100644 index 0000000000000..66909d53dd567 --- /dev/null +++ b/packages/sdk/tests/handlers/relations.test.ts @@ -0,0 +1,20 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('relations', function () { + it(`update one`, async () => { + mockServer.use(rest.patch(URL + '/relations/posts/title', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + + await expect( + sdk.relations.updateOne('posts', 'title', { + meta: { + required: true, + }, + }) + ).resolves.not.toThrowError(); + }); +}); diff --git a/packages/sdk/tests/handlers/server.test.ts b/packages/sdk/tests/handlers/server.test.ts new file mode 100644 index 0000000000000..bb9807d6f73e5 --- /dev/null +++ b/packages/sdk/tests/handlers/server.test.ts @@ -0,0 +1,40 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('server', function () { + it(`ping the server`, async () => { + mockServer.use(rest.get(URL + '/server/ping', (_req, res, ctx) => res(ctx.status(200), ctx.text('pong')))); + + const sdk = new Directus(URL); + const str = await sdk.server.ping(); + + expect(str).toBe('pong'); + }); + + it(`get server info`, async () => { + mockServer.use(rest.get(URL + '/server/info', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + await expect(sdk.server.info()).resolves.not.toThrowError(); + }); + + it(`get open api spec`, async () => { + mockServer.use( + rest.get(URL + '/server/specs/oas', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + openapi: '3.0.0', + }) + ) + ) + ); + + const sdk = new Directus(URL); + const oas = await sdk.server.oas(); + + expect(oas.openapi).toBe('3.0.0'); + }); +}); diff --git a/packages/sdk/tests/handlers/tfa.test.ts b/packages/sdk/tests/handlers/tfa.test.ts new file mode 100644 index 0000000000000..20f206d8504f8 --- /dev/null +++ b/packages/sdk/tests/handlers/tfa.test.ts @@ -0,0 +1,44 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('tfa', function () { + it(`generate`, async () => { + mockServer.use( + rest.post(URL + '/users/me/tfa/generate', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + secret: 'supersecret', + otpauth_url: 'https://example.com', + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + const data = await sdk.users.me.tfa.generate('password1234'); + + expect(data).toStrictEqual({ + secret: 'supersecret', + otpauth_url: 'https://example.com', + }); + }); + + it(`enable`, async () => { + mockServer.use(rest.post(URL + '/users/me/tfa/enable', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + await expect(sdk.users.me.tfa.enable('supersecret', '123456')).resolves.not.toThrowError(); + }); + + it(`disable`, async () => { + mockServer.use(rest.post(URL + '/users/me/tfa/disable', (_req, res, ctx) => res(ctx.status(200)))); + + const sdk = new Directus(URL); + await expect(sdk.users.me.tfa.disable('12345')).resolves.not.toThrowError(); + }); +}); diff --git a/packages/sdk/tests/handlers/utils.test.ts b/packages/sdk/tests/handlers/utils.test.ts new file mode 100644 index 0000000000000..dd007ebf64a09 --- /dev/null +++ b/packages/sdk/tests/handlers/utils.test.ts @@ -0,0 +1,81 @@ +import { Directus } from '../../src'; +import { mockServer, URL } from '../utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('utils', function () { + it(`generates random string`, async () => { + mockServer.use( + rest.get(URL + '/utils/random/string', (_req, res, ctx) => + res(ctx.status(200), ctx.json({ data: '01234567890123456789012345678901' })) + ) + ); + + const sdk = new Directus(URL); + const str = await sdk.utils.random.string(32); + + expect(str).toBe('01234567890123456789012345678901'); + }); + + it(`hash generate`, async () => { + const HASH = '$argon2i$v=19$m=16,t=2,p=1$YXNkYXNkYXNkYQ$q1exR8e4IRDiD1TR3rGB6g'; + + mockServer.use( + rest.options(URL + '/utils/hash/generate', (_r, res, ctx) => res(ctx.status(200))), + rest.post(URL + '/utils/hash/generate', async (req, res, ctx) => { + return res(ctx.status(200), ctx.json({ data: HASH })); + }) + ); + + const sdk = new Directus(URL); + const hash = await sdk.utils.hash.generate('wolfulus'); + + expect(hash).toBe(HASH); + }); + + it(`hash verify`, async () => { + const HASH = '$argon2i$v=19$m=16,t=2,p=1$YXNkYXNkYXNkYQ$q1exR8e4IRDiD1TR3rGB6g'; + + mockServer.use( + rest.options(URL + '/utils/hash/generate', (_r, res, ctx) => res(ctx.status(200))), + rest.post(URL + '/utils/hash/generate', async (req, res, ctx) => { + return res(ctx.status(200), ctx.json({ data: HASH })); + }), + rest.options(URL + '/utils/hash/verify', (_r, res, ctx) => res(ctx.status(200))), + rest.post(URL + '/utils/hash/verify', async (_r, res, ctx) => { + return res(ctx.status(200), ctx.json({ data: true })); + }) + ); + + const sdk = new Directus(URL); + const hash = await sdk.utils.hash.generate('wolfulus'); + + expect(hash).toBe(HASH); + + const result = await sdk.utils.hash.verify('wolfulus', hash || ''); + + expect(result).toBe(true); + }); + + it(`sort`, async () => { + mockServer.use( + rest.post(URL + '/utils/sort/posts', async (_r, res, ctx) => { + return res(ctx.status(204)); + }) + ); + + const sdk = new Directus(URL); + await sdk.utils.sort('posts', 10, 5); + }); + + it(`revert`, async () => { + mockServer.use( + rest.post(URL + '/utils/revert/555', async (_r, res, ctx) => { + return res(ctx.status(204)); + }) + ); + + const sdk = new Directus(URL); + await sdk.utils.revert(555); + }); +}); diff --git a/packages/sdk/tests/items.test.ts b/packages/sdk/tests/items.test.ts new file mode 100644 index 0000000000000..57d604376f1ee --- /dev/null +++ b/packages/sdk/tests/items.test.ts @@ -0,0 +1,440 @@ +import { Blog } from './blog.d'; +import { Directus, ItemsOptions, EmptyParamError } from '../src'; +import { mockServer, URL } from './utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +describe('items', function () { + it(`should throw EmptyParamError when using empty string as id`, async () => { + const sdk = new Directus(URL); + + try { + await sdk.items('posts').readOne(''); + } catch (err: any) { + expect(err).toBeInstanceOf(EmptyParamError); + } + }); + + it(`can get an item by id`, async () => { + mockServer.use( + rest.get(URL + '/items/posts/1', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + id: 1, + title: 'My first post', + body: '

Hey there!

', + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + const item = await sdk.items('posts').readOne(1); + + expect(item).not.toBeNull(); + expect(item).not.toBeUndefined(); + expect(item?.id).toBe(1); + expect(item?.title).toBe(`My first post`); + expect(item?.body).toBe('

Hey there!

'); + }); + + it(`should encode ids`, async () => { + mockServer.use( + rest.get(URL + '/items/categories/double%20slash', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + slug: 'double slash', + name: 'Double Slash', + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + const item = await sdk.items('categories').readOne('double slash'); + + expect(item).not.toBeNull(); + expect(item).not.toBeUndefined(); + expect(item?.slug).toBe('double slash'); + expect(item?.name).toBe('Double Slash'); + }); + + it(`can get multiple items by primary key`, async () => { + mockServer.use( + rest.get(URL + '/fields/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: [ + { + collection: 'posts', + field: 'primary_key', + schema: { is_primary_key: true }, + }, + ], + }) + ) + ), + rest.get(URL + '/items/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: [ + { + primary_key: 1, + title: 'My first post', + body: '

Hey there!

', + published: false, + }, + { + primary_key: 2, + title: 'My second post', + body: '

Hey there!

', + published: true, + }, + ], + }) + ) + ) + ); + + const sdk = new Directus(URL); + const items = await sdk.items('posts').readMany([1, 2]); + + expect(items.data?.[0]).toMatchObject({ + primary_key: 1, + title: 'My first post', + body: '

Hey there!

', + published: false, + }); + + expect(items.data?.[1]).toMatchObject({ + primary_key: 2, + title: 'My second post', + body: '

Hey there!

', + published: true, + }); + }); + + it(`filter param is sent`, async () => { + mockServer.use( + rest.get(URL + '/items/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: [ + { + id: 1, + title: 'My first post', + }, + { + id: 2, + title: 'My second post', + }, + ], + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const items = await sdk.items('posts').readByQuery({ + fields: ['id', 'title'], + }); + + expect(items.data?.[0]?.id).toBe(1); + expect(items.data?.[0]?.title).toBe(`My first post`); + expect((items.data?.[0])?.body).toBeUndefined(); + + expect(items.data?.[1]?.id).toBe(2); + expect(items.data?.[1]?.title).toBe(`My second post`); + expect((items.data?.[1])?.body).toBeUndefined(); + }); + + it(`create one item`, async () => { + mockServer.use( + rest.post(URL + '/items/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + id: 3, + title: 'New post', + body: 'This is a new post', + published: false, + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const item = await sdk.items('posts').createOne({ + title: 'New post', + body: 'This is a new post', + published: false, + }); + + expect(item).toMatchObject({ + id: 3, + title: 'New post', + body: 'This is a new post', + published: false, + }); + }); + + it(`create many items`, async () => { + mockServer.use( + rest.post(URL + '/items/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: [ + { + id: 4, + title: 'New post 2', + body: 'This is a new post 2', + published: false, + }, + { + id: 5, + title: 'New post 3', + body: 'This is a new post 3', + published: true, + }, + ], + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const items = await sdk.items('posts').createMany([ + { + title: 'New post 2', + body: 'This is a new post 2', + published: false, + }, + { + title: 'New post 3', + body: 'This is a new post 3', + published: true, + }, + ]); + + expect(items.data?.[0]).toMatchObject({ + id: 4, + title: 'New post 2', + body: 'This is a new post 2', + published: false, + }); + + expect(items.data?.[1]).toMatchObject({ + id: 5, + title: 'New post 3', + body: 'This is a new post 3', + published: true, + }); + }); + + it(`update one item`, async () => { + mockServer.use( + rest.patch(URL + '/items/posts/1', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + id: 1, + title: 'Updated post', + body: 'Updated post content', + published: true, + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const item = await sdk.items('posts').updateOne(1, { + title: 'Updated post', + body: 'Updated post content', + published: true, + }); + + expect(item).toMatchObject({ + title: 'Updated post', + body: 'Updated post content', + published: true, + }); + }); + + it(`update many item`, async () => { + mockServer.use( + rest.patch(URL + '/items/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: [ + { + id: 1, + title: 'Updated post', + body: 'Updated post content', + published: true, + }, + { + id: 2, + title: 'Updated post', + body: 'Updated post content', + published: true, + }, + ], + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const items = await sdk.items('posts').updateMany([1, 2], { + title: 'Updated post', + body: 'Updated post content', + published: true, + }); + + expect(items.data?.[0]).toMatchObject({ + id: 1, + title: 'Updated post', + body: 'Updated post content', + published: true, + }); + + expect(items.data?.[1]).toMatchObject({ + id: 2, + title: 'Updated post', + body: 'Updated post content', + published: true, + }); + }); + + it(`update batch`, async () => { + mockServer.use( + rest.patch(URL + '/items/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: [ + { + id: 1, + title: 'Updated post', + body: 'Updated post content', + published: true, + }, + { + id: 2, + title: 'Updated post', + body: 'Updated post content', + published: true, + }, + ], + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const items = await sdk.items('posts').updateBatch([ + { + id: 1, + title: 'Updated post', + body: 'Updated post content', + published: true, + }, + { + id: 2, + title: 'Updated post 2', + body: 'Updated post content 2', + published: true, + }, + ]); + + expect(items.data?.[0]).toMatchObject({ + id: 1, + title: 'Updated post', + body: 'Updated post content', + published: true, + }); + + expect(items.data?.[1]).toMatchObject({ + id: 2, + title: 'Updated post', + body: 'Updated post content', + published: true, + }); + }); + + it(`delete one item`, async () => { + mockServer.use(rest.delete(URL + '/items/posts/1', (_req, res, ctx) => res(ctx.status(204)))); + + const sdk = new Directus(URL); + await expect(sdk.items('posts').deleteOne(1)).resolves.not.toThrowError(); + }); + + it(`delete many item`, async () => { + mockServer.use(rest.delete(URL + '/items/posts', (_req, res, ctx) => res(ctx.status(204)))); + + const sdk = new Directus(URL); + await expect(sdk.items('posts').deleteMany([1, 2])).resolves.not.toThrowError(); + }); + + it('should passthrough additional headers', async () => { + const postData = { + title: 'New post', + body: 'This is a new post', + published: false, + }; + + const id = 3; + + const expectedData = { + id, + ...postData, + }; + + const headerName = 'X-Custom-Header'; + const headerValue = 'Custom header value'; + + const customOptions: ItemsOptions = { + requestOptions: { + headers: { + [headerName]: headerValue, + }, + }, + }; + + mockServer.use( + rest.post(URL + '/items/posts', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: expectedData, + }) + ) + ) + ); + + const sdk = new Directus(URL); + const item = await sdk.items('posts').createOne(postData, undefined, customOptions); + expect(item).toMatchObject(expectedData); + }); +}); diff --git a/packages/sdk/tests/singleton.test.ts b/packages/sdk/tests/singleton.test.ts new file mode 100644 index 0000000000000..066f968fe484d --- /dev/null +++ b/packages/sdk/tests/singleton.test.ts @@ -0,0 +1,77 @@ +import { Directus } from '../src'; +import { mockServer, URL } from './utils'; +import { describe, expect, it } from 'vitest'; +import { rest } from 'msw'; + +type Settings = { + URL: string; + copyright: string; + title: string; + ua_code: string; + show_menu: boolean; +}; + +type MyWebsite = { + settings: Settings; +}; + +describe('singleton', function () { + it(`can get an item`, async () => { + mockServer.use( + rest.get(URL + '/items/settings', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + url: 'http://website.com', + copyright: 'MyWebsite', + title: 'Website Title', + ua_code: 'UA1234567890', + show_menu: true, + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + const settings = await sdk.singleton('settings').read(); + + expect(settings).not.toBeNull(); + expect(settings).not.toBeUndefined(); + expect(settings?.url).toBe('http://website.com'); + expect(settings?.title).toBe(`Website Title`); + expect(settings?.show_menu).toBe(true); + }); + + it(`can update an item`, async () => { + mockServer.use( + rest.patch(URL + '/items/settings', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + data: { + url: 'http://website.com', + copyright: 'MyWebsite', + title: 'New Website Title', + ua_code: 'UA1234567890', + show_menu: true, + }, + }) + ) + ) + ); + + const sdk = new Directus(URL); + + const settings = await sdk.singleton('settings').update({ + title: 'New Website Title', + }); + + expect(settings).not.toBeNull(); + expect(settings).not.toBeUndefined(); + expect(settings?.url).toBe('http://website.com'); + expect(settings?.title).toBe(`New Website Title`); + expect(settings?.show_menu).toBe(true); + }); +}); diff --git a/packages/sdk/tests/tsconfig.json b/packages/sdk/tests/tsconfig.json new file mode 100644 index 0000000000000..1d9273935bf88 --- /dev/null +++ b/packages/sdk/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": ".." + }, + "include": [".", "../src"] +} diff --git a/packages/sdk/tests/utils.ts b/packages/sdk/tests/utils.ts new file mode 100644 index 0000000000000..ad4d60ae5825e --- /dev/null +++ b/packages/sdk/tests/utils.ts @@ -0,0 +1,82 @@ +import { setImmediate, setTimeout } from 'timers'; +import { afterAll, beforeAll, afterEach, vi } from 'vitest'; +import { setupServer } from 'msw/node'; + +export const URL = process.env.TEST_URL || 'http://localhost'; +export const MODE = process.env.TEST_MODE || 'dryrun'; + +export const mockServer = setupServer(); + +// Start server before all tests +beforeAll(() => mockServer.listen({ onUnhandledRequest: 'error' })); + +// Close server after all tests +afterAll(() => mockServer.close()); + +// Reset handlers after each test `important for test isolation` +afterEach(() => mockServer.resetHandlers()); + +export async function timers( + func: (opts: { + flush: () => Promise; + sleep: (ms: number) => Promise; + tick: (ms: number) => Promise; + skip: (func: () => Promise, date?: boolean) => Promise; + }) => Promise, + initial: number = Date.now() +): Promise { + const originals = { + setTimeout: setTimeout, + setImmediate: setImmediate, + }; + + vi.useFakeTimers(); + vi.setSystemTime(new Date(initial)); + + let travel = 0; + + try { + const tick = async (ms: number) => { + travel += ms; + await Promise.resolve().then(() => vi.advanceTimersByTime(ms)); + }; + + const skip = async (func: () => Promise, date = false) => { + vi.useRealTimers(); + + try { + await func(); + } finally { + if (date) { + vi.setSystemTime(initial + travel); + } + + vi.useFakeTimers(); + } + }; + + const flush = () => + new Promise((resolve) => { + vi.runAllTicks(); + + originals.setImmediate(resolve); + }); + + const sleep = (ms: number) => + new Promise((resolve) => { + travel += ms; + vi.advanceTimersByTime(travel); + originals.setTimeout(resolve, ms); + }); + + await func({ + tick, + skip, + flush, + sleep, + }); + } finally { + vi.clearAllTimers(); + vi.useRealTimers(); + } +} diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100644 index 0000000000000..2626ef2445da1 --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2019", + "lib": ["ES2019", "DOM"], + "module": "ES2015", + "moduleResolution": "node", + "declaration": true, + "declarationDir": "dist/types", + "declarationMap": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUncheckedIndexedAccess": true, + "noUnusedParameters": true, + "alwaysStrict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "resolveJsonModule": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "outDir": "dist" + }, + "include": ["./src/**/*.ts"] +} diff --git a/packages/sdk/tsconfig.prod.json b/packages/sdk/tsconfig.prod.json new file mode 100644 index 0000000000000..303fc32401531 --- /dev/null +++ b/packages/sdk/tsconfig.prod.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.test.ts"] +} diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts new file mode 100644 index 0000000000000..ad9b015524b8e --- /dev/null +++ b/packages/sdk/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3866604f97f0..20a487da66483 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -482,7 +482,7 @@ importers: version: 5.0.4 vitest: specifier: 0.31.0 - version: 0.31.0 + version: 0.31.0(happy-dom@9.8.4)(sass@1.62.0) app: dependencies: @@ -1191,6 +1191,40 @@ importers: specifier: 5.0.4 version: 5.0.4 + packages/sdk: + dependencies: + axios: + specifier: ^0.27.2 + version: 0.27.2 + devDependencies: + '@directus/tsconfig': + specifier: 0.0.7 + version: 0.0.7 + '@types/node': + specifier: ^18.0.3 + version: 18.15.13 + '@typescript-eslint/eslint-plugin': + specifier: ^5.30.5 + version: 5.30.5(@typescript-eslint/parser@5.30.5)(eslint@8.38.0)(typescript@4.7.4) + '@typescript-eslint/parser': + specifier: ^5.30.5 + version: 5.30.5(eslint@8.38.0)(typescript@4.7.4) + dotenv: + specifier: 16.0.1 + version: 16.0.1 + jsdom: + specifier: ^22.0.0 + version: 22.0.0 + msw: + specifier: ^1.2.1 + version: 1.2.1(typescript@4.7.4) + typescript: + specifier: 4.7.4 + version: 4.7.4 + vitest: + specifier: 0.31.0 + version: 0.31.0(jsdom@22.0.0) + packages/specs: dependencies: openapi3-ts: @@ -5574,6 +5608,30 @@ packages: react: 18.0.0 dev: true + /@mswjs/cookies@0.2.2: + resolution: {integrity: sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==} + engines: {node: '>=14'} + dependencies: + '@types/set-cookie-parser': 2.4.2 + set-cookie-parser: 2.6.0 + dev: true + + /@mswjs/interceptors@0.17.9: + resolution: {integrity: sha512-4LVGt03RobMH/7ZrbHqRxQrS9cc2uh+iNKSj8UWr8M26A2i793ju+csaB5zaqYltqJmA2jUq4VeYfKmVqvsXQg==} + engines: {node: '>=14'} + dependencies: + '@open-draft/until': 1.0.3 + '@types/debug': 4.1.7 + '@xmldom/xmldom': 0.8.7 + debug: 4.3.4 + headers-polyfill: 3.1.2 + outvariant: 1.4.0 + strict-event-emitter: 0.2.8 + web-encoding: 1.1.5 + transitivePeerDependencies: + - supports-color + dev: true + /@napi-rs/snappy-android-arm-eabi@7.2.2: resolution: {integrity: sha512-H7DuVkPCK5BlAr1NfSU8bDEN7gYs+R78pSHhDng83QxRnCLmVIZk33ymmIwurmoA1HrdTxbkbuNl+lMvNqnytw==} engines: {node: '>= 10'} @@ -5740,6 +5798,10 @@ packages: rimraf: 3.0.2 optional: true + /@open-draft/until@1.0.3: + resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} + dev: true + /@opentelemetry/api@1.4.1: resolution: {integrity: sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==} engines: {node: '>=8.0.0'} @@ -7535,6 +7597,10 @@ packages: pretty-format: 29.5.0 dev: true + /@types/js-levenshtein@1.1.1: + resolution: {integrity: sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==} + dev: true + /@types/js-yaml@4.0.5: resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} dev: true @@ -7776,6 +7842,12 @@ packages: '@types/node': 18.15.13 dev: true + /@types/set-cookie-parser@2.4.2: + resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} + dependencies: + '@types/node': 18.15.13 + dev: true + /@types/ssri@7.1.1: resolution: {integrity: sha512-DPP/jkDaqGiyU75MyMURxLWyYLwKSjnAuGe9ZCsLp9QZOpXmDfuevk769F0BS86TmRuD5krnp06qw9nSoNO+0g==} dependencies: @@ -7873,6 +7945,33 @@ packages: '@types/yargs-parser': 21.0.0 dev: true + /@typescript-eslint/eslint-plugin@5.30.5(@typescript-eslint/parser@5.30.5)(eslint@8.38.0)(typescript@4.7.4): + resolution: {integrity: sha512-lftkqRoBvc28VFXEoRgyZuztyVUQ04JvUnATSPtIRFAccbXTWL6DEtXGYMcbg998kXw1NLUJm7rTQ9eUt+q6Ig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.30.5(eslint@8.38.0)(typescript@4.7.4) + '@typescript-eslint/scope-manager': 5.30.5 + '@typescript-eslint/type-utils': 5.30.5(eslint@8.38.0)(typescript@4.7.4) + '@typescript-eslint/utils': 5.30.5(eslint@8.38.0)(typescript@4.7.4) + debug: 4.3.4 + eslint: 8.38.0 + functional-red-black-tree: 1.0.1 + ignore: 5.2.4 + regexpp: 3.2.0 + semver: 7.5.0 + tsutils: 3.21.0(typescript@4.7.4) + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/eslint-plugin@5.59.0(@typescript-eslint/parser@5.59.0)(eslint@8.38.0)(typescript@5.0.4): resolution: {integrity: sha512-p0QgrEyrxAWBecR56gyn3wkG15TJdI//eetInP3zYRewDh0XS+DhB3VUAd3QqvziFsfaQIoIuZMxZRB7vXYaYw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7901,6 +8000,26 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@5.30.5(eslint@8.38.0)(typescript@4.7.4): + resolution: {integrity: sha512-zj251pcPXI8GO9NDKWWmygP6+UjwWmrdf9qMW/L/uQJBM/0XbU2inxe5io/234y/RCvwpKEYjZ6c1YrXERkK4Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.30.5 + '@typescript-eslint/types': 5.30.5 + '@typescript-eslint/typescript-estree': 5.30.5(typescript@4.7.4) + debug: 4.3.4 + eslint: 8.38.0 + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@5.59.0(eslint@8.38.0)(typescript@5.0.4): resolution: {integrity: sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7921,6 +8040,14 @@ packages: - supports-color dev: true + /@typescript-eslint/scope-manager@5.30.5: + resolution: {integrity: sha512-NJ6F+YHHFT/30isRe2UTmIGGAiXKckCyMnIV58cE3JkHmaD6e5zyEYm5hBDv0Wbin+IC0T1FWJpD3YqHUG/Ydg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.30.5 + '@typescript-eslint/visitor-keys': 5.30.5 + dev: true + /@typescript-eslint/scope-manager@5.59.0: resolution: {integrity: sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7929,6 +8056,25 @@ packages: '@typescript-eslint/visitor-keys': 5.59.0 dev: true + /@typescript-eslint/type-utils@5.30.5(eslint@8.38.0)(typescript@4.7.4): + resolution: {integrity: sha512-k9+ejlv1GgwN1nN7XjVtyCgE0BTzhzT1YsQF0rv4Vfj2U9xnslBgMYYvcEYAFVdvhuEscELJsB7lDkN7WusErw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/utils': 5.30.5(eslint@8.38.0)(typescript@4.7.4) + debug: 4.3.4 + eslint: 8.38.0 + tsutils: 3.21.0(typescript@4.7.4) + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/type-utils@5.59.0(eslint@8.38.0)(typescript@5.0.4): resolution: {integrity: sha512-d/B6VSWnZwu70kcKQSCqjcXpVH+7ABKH8P1KNn4K7j5PXXuycZTPXF44Nui0TEm6rbWGi8kc78xRgOC4n7xFgA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7949,11 +8095,37 @@ packages: - supports-color dev: true + /@typescript-eslint/types@5.30.5: + resolution: {integrity: sha512-kZ80w/M2AvsbRvOr3PjaNh6qEW1LFqs2pLdo2s5R38B2HYXG8Z0PP48/4+j1QHJFL3ssHIbJ4odPRS8PlHrFfw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@typescript-eslint/types@5.59.0: resolution: {integrity: sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@typescript-eslint/typescript-estree@5.30.5(typescript@4.7.4): + resolution: {integrity: sha512-qGTc7QZC801kbYjAr4AgdOfnokpwStqyhSbiQvqGBLixniAKyH+ib2qXIVo4P9NgGzwyfD9I0nlJN7D91E1VpQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.30.5 + '@typescript-eslint/visitor-keys': 5.30.5 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.0 + tsutils: 3.21.0(typescript@4.7.4) + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@5.59.0(typescript@5.0.4): resolution: {integrity: sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7975,6 +8147,24 @@ packages: - supports-color dev: true + /@typescript-eslint/utils@5.30.5(eslint@8.38.0)(typescript@4.7.4): + resolution: {integrity: sha512-o4SSUH9IkuA7AYIfAvatldovurqTAHrfzPApOZvdUq01hHojZojCFXx06D/aFpKCgWbMPRdJBWAC3sWp3itwTA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@typescript-eslint/scope-manager': 5.30.5 + '@typescript-eslint/types': 5.30.5 + '@typescript-eslint/typescript-estree': 5.30.5(typescript@4.7.4) + eslint: 8.38.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.38.0) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils@5.59.0(eslint@8.38.0)(typescript@5.0.4): resolution: {integrity: sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7995,6 +8185,14 @@ packages: - typescript dev: true + /@typescript-eslint/visitor-keys@5.30.5: + resolution: {integrity: sha512-D+xtGo9HUMELzWIUqcQc0p2PO4NyvTrgIOK/VnSH083+8sq0tiLozNRKuLarwHYGRuA6TVBQSuuLwJUDWd3aaA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.30.5 + eslint-visitor-keys: 3.4.0 + dev: true + /@typescript-eslint/visitor-keys@5.59.0: resolution: {integrity: sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8055,7 +8253,7 @@ packages: magic-string: 0.30.0 picocolors: 1.0.0 std-env: 3.3.2 - vitest: 0.31.0 + vitest: 0.31.0(happy-dom@9.8.4)(sass@1.62.0) dev: true /@vitest/expect@0.31.0: @@ -8365,7 +8563,6 @@ packages: /@xmldom/xmldom@0.8.7: resolution: {integrity: sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==} engines: {node: '>=10.0.0'} - dev: false /@xtuc/ieee754@1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -8393,10 +8590,20 @@ packages: isexe: 2.0.0 dev: false + /@zxing/text-encoding@0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: true + optional: true + /JSV@4.0.2: resolution: {integrity: sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw==} dev: true + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: true + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -8881,6 +9088,15 @@ packages: dev: false optional: true + /axios@0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: false + /axios@1.3.6: resolution: {integrity: sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==} dependencies: @@ -9442,6 +9658,14 @@ packages: escape-string-regexp: 1.0.5 supports-color: 5.5.0 + /chalk@4.1.1: + resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -9534,6 +9758,13 @@ packages: strip-ansi: 6.0.1 dev: false + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: true + /cli-cursor@4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9543,7 +9774,6 @@ packages: /cli-spinners@2.8.0: resolution: {integrity: sha512-/eG5sJcvEIwxcdYM86k5tPwn0MUzkX5YY3eImTGpJOZgVe4SdTMY14vQpcxgBzJ0wXwAYrS8E+c3uHeK4JNyzQ==} engines: {node: '>=6'} - dev: false /cli-table3@0.6.3: resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} @@ -9562,6 +9792,11 @@ packages: string-width: 5.1.2 dev: true + /cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + dev: true + /cli-width@4.0.0: resolution: {integrity: sha512-ZksGS2xpa/bYkNzN3BAw1wEjsLV/ZKOf/CCrJ/QOBsxx6fOARIkwTutxp1XIOIohi6HKmOFjMoK/XaqDVUpEEw==} engines: {node: '>= 12'} @@ -10237,6 +10472,13 @@ packages: css-tree: 1.1.3 dev: false + /cssstyle@3.0.0: + resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} + engines: {node: '>=14'} + dependencies: + rrweb-cssom: 0.6.0 + dev: true + /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} @@ -10287,6 +10529,15 @@ packages: engines: {node: '>= 12'} dev: false + /data-urls@4.0.0: + resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==} + engines: {node: '>=14'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 12.0.1 + dev: true + /dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} dev: false @@ -10361,6 +10612,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: true + /decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} dependencies: @@ -10593,6 +10848,13 @@ packages: /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: true + /domhandler@4.3.1: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} engines: {node: '>= 4'} @@ -10630,6 +10892,11 @@ packages: engines: {node: '>=12'} dev: true + /dotenv@16.0.1: + resolution: {integrity: sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==} + engines: {node: '>=12'} + dev: true + /dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} @@ -11050,6 +11317,21 @@ packages: estraverse: 5.3.0 dev: true + /eslint-utils@3.0.0(eslint@8.38.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.38.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + /eslint-visitor-keys@3.4.0: resolution: {integrity: sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -11393,6 +11675,13 @@ packages: resolution: {integrity: sha512-LXcdgpdcVedccGg0AZqg+S8lX/FCdwXD92WNZ5k5qsb0irRhSFsBOpcJt7oevyqT2/C2nEE0zSFNdBEpj3YOSw==} dev: true + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + /figures@5.0.0: resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} engines: {node: '>=14'} @@ -11725,6 +12014,10 @@ packages: es-abstract: 1.21.2 functions-have-names: 1.2.3 + /functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + dev: true + /functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} @@ -12072,7 +12365,6 @@ packages: /graphql@16.6.0: resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - dev: false /grid-index@1.1.0: resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} @@ -12224,6 +12516,10 @@ packages: hasBin: true dev: true + /headers-polyfill@3.1.2: + resolution: {integrity: sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==} + dev: true + /helmet@6.1.5: resolution: {integrity: sha512-UgAvdoG0BhF9vcCh/j0bWtElo2ZHHk6OzC98NLCM6zK03DEVSM0vUAtT7iR+oTo2Mi6sGelAH3tL6B/uUWxV4g==} engines: {node: '>=14.0.0'} @@ -12249,6 +12545,13 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + /html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} dev: true @@ -12464,6 +12767,27 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} dev: false + /inquirer@8.2.5: + resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + /inquirer@9.1.5: resolution: {integrity: sha512-3ygAIh8gcZavV9bj6MTdYddG2zPSYswP808fKS46NOwlF0zZljVpnLCHODDqItWJDbDpLb3aouAxGaJbkxoppA==} engines: {node: '>=14.18.0'} @@ -12656,6 +12980,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + dev: true + /is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -12681,6 +13010,10 @@ packages: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} + /is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + dev: true + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -12728,6 +13061,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + /is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} dev: true @@ -12791,6 +13128,11 @@ packages: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} dev: false + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + /is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -13355,6 +13697,11 @@ packages: nopt: 6.0.0 dev: true + /js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + dev: true + /js-md4@0.3.2: resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} @@ -13432,6 +13779,44 @@ packages: - supports-color dev: true + /jsdom@22.0.0: + resolution: {integrity: sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==} + engines: {node: '>=16'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + cssstyle: 3.0.0 + data-urls: 4.0.0 + decimal.js: 10.4.3 + domexception: 4.0.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.4 + parse5: 7.1.2 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.2 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 12.0.1 + ws: 8.13.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -13920,6 +14305,14 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + /log-symbols@5.1.0: resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} engines: {node: '>=12'} @@ -14846,6 +15239,42 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /msw@1.2.1(typescript@4.7.4): + resolution: {integrity: sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==} + engines: {node: '>=14'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.4.x <= 5.0.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@mswjs/cookies': 0.2.2 + '@mswjs/interceptors': 0.17.9 + '@open-draft/until': 1.0.3 + '@types/cookie': 0.4.1 + '@types/js-levenshtein': 1.1.1 + chalk: 4.1.1 + chokidar: 3.5.3 + cookie: 0.4.2 + graphql: 16.6.0 + headers-polyfill: 3.1.2 + inquirer: 8.2.5 + is-node-process: 1.2.0 + js-levenshtein: 1.1.6 + node-fetch: 2.6.9 + outvariant: 1.4.0 + path-to-regexp: 6.2.1 + strict-event-emitter: 0.4.6 + type-fest: 2.19.0 + typescript: 4.7.4 + yargs: 17.7.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: true + /muggle-string@0.2.2: resolution: {integrity: sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==} dev: true @@ -14854,6 +15283,10 @@ packages: resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} dev: true + /mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + dev: true + /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -15274,6 +15707,10 @@ packages: resolution: {integrity: sha512-/fYevVkXRcyBiZDg6yzZbm0RuaD6i0qRfn8yr+6D0KgBMOndFPxuW10qCHpzs50nN8qKuv78k8MuotZhcVX6Pw==} dev: true + /nwsapi@2.2.4: + resolution: {integrity: sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==} + dev: true + /oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} dev: false @@ -15410,6 +15847,21 @@ packages: word-wrap: 1.2.3 dev: true + /ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.8.0 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + dev: true + /ora@6.3.0: resolution: {integrity: sha512-1/D8uRFY0ay2kgBpmAwmSA404w4OoPVhHMqRqtjvrcK/dnzcEZxMJ+V4DUbyICu8IIVRclHcOf5wlD1tMY4GUQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -15441,6 +15893,10 @@ packages: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} dev: true + /outvariant@1.4.0: + resolution: {integrity: sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==} + dev: true + /p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -15605,6 +16061,12 @@ packages: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} dev: false + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -15667,6 +16129,10 @@ packages: /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + /path-to-regexp@6.2.1: + resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + dev: true + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -16493,8 +16959,6 @@ packages: /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - dev: false - optional: true /pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -16679,6 +17143,10 @@ packages: strict-uri-encode: 2.0.0 dev: false + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -16992,6 +17460,11 @@ packages: define-properties: 1.2.0 functions-have-names: 1.2.3 + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + /regexpu-core@5.3.2: resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} engines: {node: '>=4'} @@ -17143,6 +17616,10 @@ packages: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} dev: true + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + /resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} dev: true @@ -17187,6 +17664,14 @@ packages: lowercase-keys: 2.0.0 dev: true + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + /restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -17402,10 +17887,13 @@ packages: optionalDependencies: fsevents: 2.3.2 + /rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + dev: true + /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} - dev: false /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -17504,6 +17992,13 @@ packages: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} dev: false + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + /scheduler@0.21.0: resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} dependencies: @@ -17608,6 +18103,10 @@ packages: /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + /set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + dev: true + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -18131,6 +18630,16 @@ packages: engines: {node: '>=10.0.0'} dev: false + /strict-event-emitter@0.2.8: + resolution: {integrity: sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==} + dependencies: + events: 3.3.0 + dev: true + + /strict-event-emitter@0.4.6: + resolution: {integrity: sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==} + dev: true + /strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -18486,6 +18995,10 @@ packages: - utf-8-validate dev: true + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + /synchronous-promise@2.0.17: resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} dev: true @@ -18683,7 +19196,6 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: false /tildify@2.0.0: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} @@ -18791,9 +19303,26 @@ packages: dev: false optional: true + /tough-cookie@4.1.2: + resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.0 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + /tr46@4.1.1: + resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} + engines: {node: '>=14'} + dependencies: + punycode: 2.3.0 + dev: true + /traverse@0.6.7: resolution: {integrity: sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==} dev: true @@ -18861,6 +19390,16 @@ packages: /tslib@2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + /tsutils@3.21.0(typescript@4.7.4): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.7.4 + dev: true + /tsutils@3.21.0(typescript@5.0.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -19007,6 +19546,12 @@ packages: /typedarray@0.0.7: resolution: {integrity: sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ==} + /typescript@4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /typescript@5.0.4: resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} @@ -19227,6 +19772,11 @@ packages: engines: {node: '>= 4.0.0'} dev: true + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -19269,6 +19819,13 @@ packages: dev: false optional: true + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + /use-resize-observer@9.1.0(react-dom@18.0.0)(react@18.0.0): resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} peerDependencies: @@ -19443,27 +20000,6 @@ packages: vfile-message: 3.1.4 dev: true - /vite-node@0.31.0(@types/node@18.15.13): - resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==} - engines: {node: '>=v14.18.0'} - hasBin: true - dependencies: - cac: 6.7.14 - debug: 4.3.4 - mlly: 1.2.0 - pathe: 1.1.0 - picocolors: 1.0.0 - vite: 4.3.1(@types/node@18.15.13) - transitivePeerDependencies: - - '@types/node' - - less - - sass - - stylus - - sugarss - - supports-color - - terser - dev: true - /vite-node@0.31.0(@types/node@18.15.13)(sass@1.62.0): resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==} engines: {node: '>=v14.18.0'} @@ -19485,39 +20021,6 @@ packages: - terser dev: true - /vite@4.3.1(@types/node@18.15.13): - resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 18.15.13 - esbuild: 0.17.17 - postcss: 8.4.23 - rollup: 3.20.7 - optionalDependencies: - fsevents: 2.3.2 - dev: true - /vite@4.3.1(@types/node@18.15.13)(sass@1.62.0): resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -19580,7 +20083,7 @@ packages: - terser dev: true - /vitest@0.31.0: + /vitest@0.31.0(happy-dom@9.8.4)(sass@1.62.0): resolution: {integrity: sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==} engines: {node: '>=v14.18.0'} hasBin: true @@ -19625,6 +20128,7 @@ packages: chai: 4.3.7 concordance: 5.0.4 debug: 4.3.4 + happy-dom: 9.8.4 local-pkg: 0.4.3 magic-string: 0.30.0 pathe: 1.1.0 @@ -19633,8 +20137,8 @@ packages: strip-literal: 1.0.1 tinybench: 2.4.0 tinypool: 0.5.0 - vite: 4.3.1(@types/node@18.15.13) - vite-node: 0.31.0(@types/node@18.15.13) + vite: 4.3.1(@types/node@18.15.13)(sass@1.62.0) + vite-node: 0.31.0(@types/node@18.15.13)(sass@1.62.0) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -19645,7 +20149,7 @@ packages: - terser dev: true - /vitest@0.31.0(happy-dom@9.8.4)(sass@1.62.0): + /vitest@0.31.0(jsdom@22.0.0): resolution: {integrity: sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==} engines: {node: '>=v14.18.0'} hasBin: true @@ -19690,7 +20194,7 @@ packages: chai: 4.3.7 concordance: 5.0.4 debug: 4.3.4 - happy-dom: 9.8.4 + jsdom: 22.0.0 local-pkg: 0.4.3 magic-string: 0.30.0 pathe: 1.1.0 @@ -19859,6 +20363,13 @@ packages: vue: 3.3.2 dev: true + /w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -19878,6 +20389,14 @@ packages: dependencies: defaults: 1.0.4 + /web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: true + /web-streams-polyfill@3.2.1: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} @@ -19968,6 +20487,14 @@ packages: engines: {node: '>=12'} dev: true + /whatwg-url@12.0.1: + resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==} + engines: {node: '>=14'} + dependencies: + tr46: 4.1.1 + webidl-conversions: 7.0.0 + dev: true + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -20219,6 +20746,10 @@ packages: engines: {node: '>=8.0'} dev: true + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + /xmlcreate@2.0.4: resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==} dev: false