diff --git a/.fernignore b/.fernignore new file mode 100644 index 0000000..084a8eb --- /dev/null +++ b/.fernignore @@ -0,0 +1 @@ +# Specify files that shouldn't be modified by Fern diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e2b82d1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up node + uses: actions/setup-node@v3 + + - name: Compile + run: yarn && yarn build + + publish: + needs: [ compile ] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up node + uses: actions/setup-node@v3 + + - name: Install dependencies + run: yarn install + + - name: Build + run: yarn build + + - name: Publish to npm + run: | + npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} + npm publish --access public + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1498321 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules +.DS_Store +/dist +/Client.d.ts +/Client.js +/environments.d.ts +/environments.js +/index.d.ts +/index.js +/api +/core +/errors +/serialization \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e62938d --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +node_modules +src +.gitignore +.github +.fernignore +.prettierrc.yml +tsconfig.json +yarn.lock \ No newline at end of file diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..0c06786 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,2 @@ +tabWidth: 4 +printWidth: 120 diff --git a/README.md b/README.md deleted file mode 100644 index a89e2f0..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# syndicate-node \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..7b8ce3c --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "@syndicateio/syndicate-node", + "version": "1.0.0", + "private": false, + "repository": "https://github.com/SyndicateProtocol/syndicate-node", + "main": "./index.js", + "types": "./index.d.ts", + "scripts": { + "format": "prettier --write 'src/**/*.ts'", + "build": "tsc", + "prepack": "cp -rv dist/. ." + }, + "dependencies": { + "url-join": "4.0.1", + "@types/url-join": "4.0.1", + "@ungap/url-search-params": "0.2.2", + "js-base64": "3.7.2", + "axios": "0.27.2" + }, + "devDependencies": { + "@types/node": "17.0.33", + "prettier": "2.7.1", + "typescript": "4.6.4" + } +} \ No newline at end of file diff --git a/src/Client.ts b/src/Client.ts new file mode 100644 index 0000000..975dc58 --- /dev/null +++ b/src/Client.ts @@ -0,0 +1,33 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as core from "./core"; +import { Transact } from "./api/resources/transact/client/Client"; +import { Wallet } from "./api/resources/wallet/client/Client"; + +export declare namespace SyndicateClient { + interface Options { + token: core.Supplier; + } + + interface RequestOptions { + timeoutInSeconds?: number; + } +} + +export class SyndicateClient { + constructor(protected readonly _options: SyndicateClient.Options) {} + + protected _transact: Transact | undefined; + + public get transact(): Transact { + return (this._transact ??= new Transact(this._options)); + } + + protected _wallet: Wallet | undefined; + + public get wallet(): Wallet { + return (this._wallet ??= new Wallet(this._options)); + } +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..3e5335f --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1 @@ +export * from "./resources"; diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts new file mode 100644 index 0000000..e3d6383 --- /dev/null +++ b/src/api/resources/index.ts @@ -0,0 +1,2 @@ +export * as transact from "./transact"; +export * as wallet from "./wallet"; diff --git a/src/api/resources/transact/client/Client.ts b/src/api/resources/transact/client/Client.ts new file mode 100644 index 0000000..3c24105 --- /dev/null +++ b/src/api/resources/transact/client/Client.ts @@ -0,0 +1,110 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as core from "../../../../core"; +import * as Syndicate from "../../.."; +import * as serializers from "../../../../serialization"; +import * as environments from "../../../../environments"; +import urlJoin from "url-join"; +import * as errors from "../../../../errors"; + +export declare namespace Transact { + interface Options { + token: core.Supplier; + } + + interface RequestOptions { + timeoutInSeconds?: number; + } +} + +export class Transact { + constructor(protected readonly _options: Transact.Options) {} + + /** + * Send transaction to blockchain + * @throws {@link Syndicate.transact.MalformedFunctionDataError} + * @throws {@link Syndicate.transact.ImATeapotError} + * @throws {@link Syndicate.transact.InternalError} + * @throws {@link Syndicate.transact.InvalidRequestIdError} + */ + public async sendTransaction( + request: Syndicate.transact.SendTransactionRequest, + requestOptions?: Transact.RequestOptions + ): Promise { + const _response = await core.fetcher({ + url: urlJoin(environments.SyndicateEnvironment.Production, "/transact/sendTransaction"), + method: "POST", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@syndicateio/syndicate-node", + "X-Fern-SDK-Version": "1.0.0", + }, + contentType: "application/json", + body: await serializers.transact.SendTransactionRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "strip", + }), + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + }); + if (_response.ok) { + return await serializers.transact.SendTransactionResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new Syndicate.transact.MalformedFunctionDataError( + await serializers.transact.ErrorWithMessage.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }) + ); + case 418: + throw new Syndicate.transact.ImATeapotError(); + case 500: + throw new Syndicate.transact.InternalError(); + case 422: + throw new Syndicate.transact.InvalidRequestIdError( + await serializers.transact.ErrorWithMessage.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }) + ); + default: + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SyndicateTimeoutError(); + case "unknown": + throw new errors.SyndicateError({ + message: _response.error.errorMessage, + }); + } + } + + protected async _getAuthorizationHeader() { + return `Bearer ${await core.Supplier.get(this._options.token)}`; + } +} diff --git a/src/api/resources/transact/client/index.ts b/src/api/resources/transact/client/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/api/resources/transact/client/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/api/resources/transact/errors/ImATeapotError.ts b/src/api/resources/transact/errors/ImATeapotError.ts new file mode 100644 index 0000000..6a9b31f --- /dev/null +++ b/src/api/resources/transact/errors/ImATeapotError.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as errors from "../../../../errors"; + +export class ImATeapotError extends errors.SyndicateError { + constructor() { + super({ + statusCode: 418, + }); + Object.setPrototypeOf(this, ImATeapotError.prototype); + } +} diff --git a/src/api/resources/transact/errors/InternalError.ts b/src/api/resources/transact/errors/InternalError.ts new file mode 100644 index 0000000..b75cd51 --- /dev/null +++ b/src/api/resources/transact/errors/InternalError.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as errors from "../../../../errors"; + +export class InternalError extends errors.SyndicateError { + constructor() { + super({ + statusCode: 500, + }); + Object.setPrototypeOf(this, InternalError.prototype); + } +} diff --git a/src/api/resources/transact/errors/InvalidRequestIdError.ts b/src/api/resources/transact/errors/InvalidRequestIdError.ts new file mode 100644 index 0000000..9869eb6 --- /dev/null +++ b/src/api/resources/transact/errors/InvalidRequestIdError.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as errors from "../../../../errors"; +import * as Syndicate from "../../.."; + +export class InvalidRequestIdError extends errors.SyndicateError { + constructor(body: Syndicate.transact.ErrorWithMessage) { + super({ + statusCode: 422, + body: body, + }); + Object.setPrototypeOf(this, InvalidRequestIdError.prototype); + } +} diff --git a/src/api/resources/transact/errors/MalformedFunctionDataError.ts b/src/api/resources/transact/errors/MalformedFunctionDataError.ts new file mode 100644 index 0000000..17deab2 --- /dev/null +++ b/src/api/resources/transact/errors/MalformedFunctionDataError.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as errors from "../../../../errors"; +import * as Syndicate from "../../.."; + +export class MalformedFunctionDataError extends errors.SyndicateError { + constructor(body: Syndicate.transact.ErrorWithMessage) { + super({ + statusCode: 400, + body: body, + }); + Object.setPrototypeOf(this, MalformedFunctionDataError.prototype); + } +} diff --git a/src/api/resources/transact/errors/index.ts b/src/api/resources/transact/errors/index.ts new file mode 100644 index 0000000..4f14791 --- /dev/null +++ b/src/api/resources/transact/errors/index.ts @@ -0,0 +1,4 @@ +export * from "./ImATeapotError"; +export * from "./InternalError"; +export * from "./MalformedFunctionDataError"; +export * from "./InvalidRequestIdError"; diff --git a/src/api/resources/transact/index.ts b/src/api/resources/transact/index.ts new file mode 100644 index 0000000..9dc8224 --- /dev/null +++ b/src/api/resources/transact/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./errors"; +export * from "./client"; diff --git a/src/api/resources/transact/types/ErrorWithMessage.ts b/src/api/resources/transact/types/ErrorWithMessage.ts new file mode 100644 index 0000000..a6d7d14 --- /dev/null +++ b/src/api/resources/transact/types/ErrorWithMessage.ts @@ -0,0 +1,7 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface ErrorWithMessage { + message: string; +} diff --git a/src/api/resources/transact/types/SendTransactionRequest.ts b/src/api/resources/transact/types/SendTransactionRequest.ts new file mode 100644 index 0000000..38d1ae8 --- /dev/null +++ b/src/api/resources/transact/types/SendTransactionRequest.ts @@ -0,0 +1,18 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface SendTransactionRequest { + /** (Optional) ID of the request. Needs to be a valid UUID. If provided, it will be saved and returned as the transactionId of the response. If not provided, we will generate one for you and return it as the transactionId. */ + requestId?: string; + /** ID of the project you want this request to be sent from */ + projectId: string; + /** The contract address to send request to */ + contractAddress: string; + /** The chain ID for the network (e.g. 1 for Ethereum Mainnet, 137 for Polygon Mainnet, 80001 for Polygon Mumbai). For a complete list of chain IDs, see [ChainList](https://chainlist.org/?search=&testnets=true). */ + chainId: number; + /** The human readable signature to call on the contract */ + functionSignature: string; + /** (Optional) The function arguments for the transaction if any. The keys are the argument names or index from the provided function signature and the values are the argument values. */ + args?: Record; +} diff --git a/src/api/resources/transact/types/SendTransactionResponse.ts b/src/api/resources/transact/types/SendTransactionResponse.ts new file mode 100644 index 0000000..79f01eb --- /dev/null +++ b/src/api/resources/transact/types/SendTransactionResponse.ts @@ -0,0 +1,7 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface SendTransactionResponse { + transactionId: string; +} diff --git a/src/api/resources/transact/types/index.ts b/src/api/resources/transact/types/index.ts new file mode 100644 index 0000000..92e394f --- /dev/null +++ b/src/api/resources/transact/types/index.ts @@ -0,0 +1,3 @@ +export * from "./ErrorWithMessage"; +export * from "./SendTransactionRequest"; +export * from "./SendTransactionResponse"; diff --git a/src/api/resources/wallet/client/Client.ts b/src/api/resources/wallet/client/Client.ts new file mode 100644 index 0000000..51be7c4 --- /dev/null +++ b/src/api/resources/wallet/client/Client.ts @@ -0,0 +1,480 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as core from "../../../../core"; +import * as Syndicate from "../../.."; +import * as serializers from "../../../../serialization"; +import * as environments from "../../../../environments"; +import urlJoin from "url-join"; +import * as errors from "../../../../errors"; +import { default as URLSearchParams } from "@ungap/url-search-params"; + +export declare namespace Wallet { + interface Options { + token: core.Supplier; + } + + interface RequestOptions { + timeoutInSeconds?: number; + } +} + +export class Wallet { + constructor(protected readonly _options: Wallet.Options) {} + + /** + * Creates a new wallet and assigns it to a project + * @throws {@link Syndicate.wallet.CreateWalletError} + */ + public async createWallet( + request: Syndicate.wallet.CreateWalletRequest, + requestOptions?: Wallet.RequestOptions + ): Promise { + const _response = await core.fetcher({ + url: urlJoin(environments.SyndicateEnvironment.Production, "/wallet/create"), + method: "POST", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@syndicateio/syndicate-node", + "X-Fern-SDK-Version": "1.0.0", + }, + contentType: "application/json", + body: await serializers.wallet.CreateWalletRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "strip", + }), + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + }); + if (_response.ok) { + return await serializers.wallet.CreateWalletResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 500: + throw new Syndicate.wallet.CreateWalletError( + await serializers.wallet.CreateWalletErrorBody.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }) + ); + default: + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SyndicateTimeoutError(); + case "unknown": + throw new errors.SyndicateError({ + message: _response.error.errorMessage, + }); + } + } + + /** + * Retires a wallet from usage + * @throws {@link Syndicate.wallet.RetireWalletError} + */ + public async retireWallet( + request: Syndicate.wallet.RetireWalletRequest, + requestOptions?: Wallet.RequestOptions + ): Promise { + const _response = await core.fetcher({ + url: urlJoin(environments.SyndicateEnvironment.Production, "/wallet/retire"), + method: "POST", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@syndicateio/syndicate-node", + "X-Fern-SDK-Version": "1.0.0", + }, + contentType: "application/json", + body: await serializers.wallet.RetireWalletRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "strip", + }), + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + }); + if (_response.ok) { + return await serializers.wallet.RetireWalletResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 500: + throw new Syndicate.wallet.RetireWalletError(); + default: + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SyndicateTimeoutError(); + case "unknown": + throw new errors.SyndicateError({ + message: _response.error.errorMessage, + }); + } + } + + /** + * Get a transaction request by id + * @throws {@link Syndicate.wallet.TransactionNotFoundError} + */ + public async getTransactionRequest( + projectId: string, + transactionId: string, + requestOptions?: Wallet.RequestOptions + ): Promise { + const _response = await core.fetcher({ + url: urlJoin( + environments.SyndicateEnvironment.Production, + `/wallet/project/${projectId}/request/${transactionId}` + ), + method: "GET", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@syndicateio/syndicate-node", + "X-Fern-SDK-Version": "1.0.0", + }, + contentType: "application/json", + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + }); + if (_response.ok) { + return await serializers.wallet.TransactionResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 404: + throw new Syndicate.wallet.TransactionNotFoundError(); + default: + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SyndicateTimeoutError(); + case "unknown": + throw new errors.SyndicateError({ + message: _response.error.errorMessage, + }); + } + } + + /** + * Gets a list of transaction requests for a project + * @throws {@link Syndicate.wallet.TransactionNotFoundError} + */ + public async getTransactionRequestsByProjects( + projectId: string, + request: Syndicate.wallet.GetRequestsByProjectIdRequest = {}, + requestOptions?: Wallet.RequestOptions + ): Promise { + const { page, limit, invalid } = request; + const _queryParams = new URLSearchParams(); + if (page != null) { + _queryParams.append("page", page.toString()); + } + + if (limit != null) { + _queryParams.append("limit", limit.toString()); + } + + if (invalid != null) { + _queryParams.append("invalid", invalid.toString()); + } + + const _response = await core.fetcher({ + url: urlJoin(environments.SyndicateEnvironment.Production, `/wallet/project/${projectId}/requests`), + method: "GET", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@syndicateio/syndicate-node", + "X-Fern-SDK-Version": "1.0.0", + }, + contentType: "application/json", + queryParameters: _queryParams, + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + }); + if (_response.ok) { + return await serializers.wallet.TransactionRequestsByProjectResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 404: + throw new Syndicate.wallet.TransactionNotFoundError(); + default: + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SyndicateTimeoutError(); + case "unknown": + throw new errors.SyndicateError({ + message: _response.error.errorMessage, + }); + } + } + + /** + * Gets a list of wallets for a project + * @throws {@link Syndicate.wallet.GetWalletsError} + */ + public async getWalletsByProject( + projectId: string, + request: Syndicate.wallet.GetWalletsByProjectIdRequest = {}, + requestOptions?: Wallet.RequestOptions + ): Promise { + const { withData } = request; + const _queryParams = new URLSearchParams(); + if (withData != null) { + _queryParams.append("withData", withData.toString()); + } + + const _response = await core.fetcher({ + url: urlJoin(environments.SyndicateEnvironment.Production, `/wallet/project/${projectId}/wallets`), + method: "GET", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@syndicateio/syndicate-node", + "X-Fern-SDK-Version": "1.0.0", + }, + contentType: "application/json", + queryParameters: _queryParams, + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + }); + if (_response.ok) { + return await serializers.wallet.getWalletsByProject.Response.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 500: + throw new Syndicate.wallet.GetWalletsError(); + default: + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SyndicateTimeoutError(); + case "unknown": + throw new errors.SyndicateError({ + message: _response.error.errorMessage, + }); + } + } + + /** + * Gets a list of transactions for a project + */ + public async getTransactionsByProject( + projectId: string, + request: Syndicate.wallet.GetTransactionsByProjectIdRequest = {}, + requestOptions?: Wallet.RequestOptions + ): Promise { + const { search, page, limit, reverted, status } = request; + const _queryParams = new URLSearchParams(); + if (search != null) { + _queryParams.append("search", search); + } + + if (page != null) { + _queryParams.append("page", page.toString()); + } + + if (limit != null) { + _queryParams.append("limit", limit.toString()); + } + + if (reverted != null) { + _queryParams.append("reverted", reverted.toString()); + } + + if (status != null) { + if (Array.isArray(status)) { + for (const _item of status) { + _queryParams.append("status", _item); + } + } else { + _queryParams.append("status", status); + } + } + + const _response = await core.fetcher({ + url: urlJoin(environments.SyndicateEnvironment.Production, `/wallet/project/${projectId}/transactions`), + method: "GET", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@syndicateio/syndicate-node", + "X-Fern-SDK-Version": "1.0.0", + }, + contentType: "application/json", + queryParameters: _queryParams, + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + }); + if (_response.ok) { + return await serializers.wallet.TransactionsByProjectResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SyndicateTimeoutError(); + case "unknown": + throw new errors.SyndicateError({ + message: _response.error.errorMessage, + }); + } + } + + /** + * Get activity for a project + */ + public async getProjectTransactionStats( + projectId: string, + requestOptions?: Wallet.RequestOptions + ): Promise { + const _response = await core.fetcher({ + url: urlJoin(environments.SyndicateEnvironment.Production, `/wallet/project/${projectId}/transactionStats`), + method: "GET", + headers: { + Authorization: await this._getAuthorizationHeader(), + "X-Fern-Language": "JavaScript", + "X-Fern-SDK-Name": "@syndicateio/syndicate-node", + "X-Fern-SDK-Version": "1.0.0", + }, + contentType: "application/json", + timeoutMs: requestOptions?.timeoutInSeconds != null ? requestOptions.timeoutInSeconds * 1000 : 60000, + }); + if (_response.ok) { + return await serializers.wallet.ProjectTransactionStatsResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + breadcrumbsPrefix: ["response"], + }); + } + + if (_response.error.reason === "status-code") { + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + }); + } + + switch (_response.error.reason) { + case "non-json": + throw new errors.SyndicateError({ + statusCode: _response.error.statusCode, + body: _response.error.rawBody, + }); + case "timeout": + throw new errors.SyndicateTimeoutError(); + case "unknown": + throw new errors.SyndicateError({ + message: _response.error.errorMessage, + }); + } + } + + protected async _getAuthorizationHeader() { + return `Bearer ${await core.Supplier.get(this._options.token)}`; + } +} diff --git a/src/api/resources/wallet/client/index.ts b/src/api/resources/wallet/client/index.ts new file mode 100644 index 0000000..415726b --- /dev/null +++ b/src/api/resources/wallet/client/index.ts @@ -0,0 +1 @@ +export * from "./requests"; diff --git a/src/api/resources/wallet/client/requests/GetRequestsByProjectIdRequest.ts b/src/api/resources/wallet/client/requests/GetRequestsByProjectIdRequest.ts new file mode 100644 index 0000000..a4365cc --- /dev/null +++ b/src/api/resources/wallet/client/requests/GetRequestsByProjectIdRequest.ts @@ -0,0 +1,9 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface GetRequestsByProjectIdRequest { + page?: number; + limit?: number; + invalid?: boolean; +} diff --git a/src/api/resources/wallet/client/requests/GetTransactionsByProjectIdRequest.ts b/src/api/resources/wallet/client/requests/GetTransactionsByProjectIdRequest.ts new file mode 100644 index 0000000..86569a8 --- /dev/null +++ b/src/api/resources/wallet/client/requests/GetTransactionsByProjectIdRequest.ts @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as Syndicate from "../../../.."; + +export interface GetTransactionsByProjectIdRequest { + search?: string; + page?: number; + limit?: number; + reverted?: boolean; + status?: Syndicate.wallet.TransactionStatus | Syndicate.wallet.TransactionStatus[]; +} diff --git a/src/api/resources/wallet/client/requests/GetWalletsByProjectIdRequest.ts b/src/api/resources/wallet/client/requests/GetWalletsByProjectIdRequest.ts new file mode 100644 index 0000000..0ff4d19 --- /dev/null +++ b/src/api/resources/wallet/client/requests/GetWalletsByProjectIdRequest.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface GetWalletsByProjectIdRequest { + /** + * Optional parameter to return this address's balance and total count of confirmed transactions + */ + withData?: boolean; +} diff --git a/src/api/resources/wallet/client/requests/index.ts b/src/api/resources/wallet/client/requests/index.ts new file mode 100644 index 0000000..bfa09b0 --- /dev/null +++ b/src/api/resources/wallet/client/requests/index.ts @@ -0,0 +1,3 @@ +export { GetRequestsByProjectIdRequest } from "./GetRequestsByProjectIdRequest"; +export { GetWalletsByProjectIdRequest } from "./GetWalletsByProjectIdRequest"; +export { GetTransactionsByProjectIdRequest } from "./GetTransactionsByProjectIdRequest"; diff --git a/src/api/resources/wallet/errors/CreateWalletError.ts b/src/api/resources/wallet/errors/CreateWalletError.ts new file mode 100644 index 0000000..792fb5e --- /dev/null +++ b/src/api/resources/wallet/errors/CreateWalletError.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as errors from "../../../../errors"; +import * as Syndicate from "../../.."; + +export class CreateWalletError extends errors.SyndicateError { + constructor(body: Syndicate.wallet.CreateWalletErrorBody) { + super({ + statusCode: 500, + body: body, + }); + Object.setPrototypeOf(this, CreateWalletError.prototype); + } +} diff --git a/src/api/resources/wallet/errors/GetWalletsError.ts b/src/api/resources/wallet/errors/GetWalletsError.ts new file mode 100644 index 0000000..e7d9cc3 --- /dev/null +++ b/src/api/resources/wallet/errors/GetWalletsError.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as errors from "../../../../errors"; + +export class GetWalletsError extends errors.SyndicateError { + constructor() { + super({ + statusCode: 500, + }); + Object.setPrototypeOf(this, GetWalletsError.prototype); + } +} diff --git a/src/api/resources/wallet/errors/RetireWalletError.ts b/src/api/resources/wallet/errors/RetireWalletError.ts new file mode 100644 index 0000000..4a07a6a --- /dev/null +++ b/src/api/resources/wallet/errors/RetireWalletError.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as errors from "../../../../errors"; + +export class RetireWalletError extends errors.SyndicateError { + constructor() { + super({ + statusCode: 500, + }); + Object.setPrototypeOf(this, RetireWalletError.prototype); + } +} diff --git a/src/api/resources/wallet/errors/TransactionNotFoundError.ts b/src/api/resources/wallet/errors/TransactionNotFoundError.ts new file mode 100644 index 0000000..0bf039f --- /dev/null +++ b/src/api/resources/wallet/errors/TransactionNotFoundError.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as errors from "../../../../errors"; + +export class TransactionNotFoundError extends errors.SyndicateError { + constructor() { + super({ + statusCode: 404, + }); + Object.setPrototypeOf(this, TransactionNotFoundError.prototype); + } +} diff --git a/src/api/resources/wallet/errors/index.ts b/src/api/resources/wallet/errors/index.ts new file mode 100644 index 0000000..9cdf9d7 --- /dev/null +++ b/src/api/resources/wallet/errors/index.ts @@ -0,0 +1,4 @@ +export * from "./CreateWalletError"; +export * from "./RetireWalletError"; +export * from "./TransactionNotFoundError"; +export * from "./GetWalletsError"; diff --git a/src/api/resources/wallet/index.ts b/src/api/resources/wallet/index.ts new file mode 100644 index 0000000..9dc8224 --- /dev/null +++ b/src/api/resources/wallet/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./errors"; +export * from "./client"; diff --git a/src/api/resources/wallet/types/CreateWalletErrorBody.ts b/src/api/resources/wallet/types/CreateWalletErrorBody.ts new file mode 100644 index 0000000..1e49240 --- /dev/null +++ b/src/api/resources/wallet/types/CreateWalletErrorBody.ts @@ -0,0 +1,8 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface CreateWalletErrorBody { + projectId: string; + message: string; +} diff --git a/src/api/resources/wallet/types/CreateWalletRequest.ts b/src/api/resources/wallet/types/CreateWalletRequest.ts new file mode 100644 index 0000000..e419cf6 --- /dev/null +++ b/src/api/resources/wallet/types/CreateWalletRequest.ts @@ -0,0 +1,8 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface CreateWalletRequest { + projectId: string; + chainId: number; +} diff --git a/src/api/resources/wallet/types/CreateWalletResponse.ts b/src/api/resources/wallet/types/CreateWalletResponse.ts new file mode 100644 index 0000000..a26c34d --- /dev/null +++ b/src/api/resources/wallet/types/CreateWalletResponse.ts @@ -0,0 +1,8 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface CreateWalletResponse { + projectId: string; + walletAddress: string; +} diff --git a/src/api/resources/wallet/types/ProjectTransactionStatsResponse.ts b/src/api/resources/wallet/types/ProjectTransactionStatsResponse.ts new file mode 100644 index 0000000..0e64b2d --- /dev/null +++ b/src/api/resources/wallet/types/ProjectTransactionStatsResponse.ts @@ -0,0 +1,8 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface ProjectTransactionStatsResponse { + numberOfTransactions: number; + numberOfFailedTransactions: number; +} diff --git a/src/api/resources/wallet/types/RetireWalletRequest.ts b/src/api/resources/wallet/types/RetireWalletRequest.ts new file mode 100644 index 0000000..7461b78 --- /dev/null +++ b/src/api/resources/wallet/types/RetireWalletRequest.ts @@ -0,0 +1,9 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface RetireWalletRequest { + projectId: string; + walletAddress: string; + gasRecipient?: string; +} diff --git a/src/api/resources/wallet/types/RetireWalletResponse.ts b/src/api/resources/wallet/types/RetireWalletResponse.ts new file mode 100644 index 0000000..f907d4b --- /dev/null +++ b/src/api/resources/wallet/types/RetireWalletResponse.ts @@ -0,0 +1,9 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface RetireWalletResponse { + projectId: string; + walletAddress: string; + transactionId?: string; +} diff --git a/src/api/resources/wallet/types/TransactionAttempt.ts b/src/api/resources/wallet/types/TransactionAttempt.ts new file mode 100644 index 0000000..9faaff5 --- /dev/null +++ b/src/api/resources/wallet/types/TransactionAttempt.ts @@ -0,0 +1,19 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as Syndicate from "../../.."; + +export interface TransactionAttempt { + transactionId: string; + hash: string; + chainId: number; + status: Syndicate.wallet.TransactionStatus; + block: number; + signedTxn: string; + walletAddress: string; + reverted: boolean; + nonce: number; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/api/resources/wallet/types/TransactionRequestsByProjectResponse.ts b/src/api/resources/wallet/types/TransactionRequestsByProjectResponse.ts new file mode 100644 index 0000000..925d2cc --- /dev/null +++ b/src/api/resources/wallet/types/TransactionRequestsByProjectResponse.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as Syndicate from "../../.."; + +export interface TransactionRequestsByProjectResponse { + transactionRequests: Syndicate.wallet.TransactionResponse[]; + total: number; +} diff --git a/src/api/resources/wallet/types/TransactionResponse.ts b/src/api/resources/wallet/types/TransactionResponse.ts new file mode 100644 index 0000000..50cef9b --- /dev/null +++ b/src/api/resources/wallet/types/TransactionResponse.ts @@ -0,0 +1,19 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as Syndicate from "../../.."; + +export interface TransactionResponse { + transactionId: string; + chainId: number; + projectId: string; + invalid: boolean; + contractAddress: string; + functionSignature: string; + data: string; + value: string; + createdAt: Date; + updatedAt: Date; + transactionAttempts?: Syndicate.wallet.TransactionAttempt[]; +} diff --git a/src/api/resources/wallet/types/TransactionStatus.ts b/src/api/resources/wallet/types/TransactionStatus.ts new file mode 100644 index 0000000..6742d50 --- /dev/null +++ b/src/api/resources/wallet/types/TransactionStatus.ts @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export type TransactionStatus = "PENDING" | "SUBMITTED" | "CONFIRMED"; + +export const TransactionStatus = { + Pending: "PENDING", + Submitted: "SUBMITTED", + Confirmed: "CONFIRMED", +} as const; diff --git a/src/api/resources/wallet/types/TransactionsByProjectResponse.ts b/src/api/resources/wallet/types/TransactionsByProjectResponse.ts new file mode 100644 index 0000000..5780eba --- /dev/null +++ b/src/api/resources/wallet/types/TransactionsByProjectResponse.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as Syndicate from "../../.."; + +export interface TransactionsByProjectResponse { + transactionAttempts: Syndicate.wallet.TransactionAttempt[]; + total: number; +} diff --git a/src/api/resources/wallet/types/Wallet.ts b/src/api/resources/wallet/types/Wallet.ts new file mode 100644 index 0000000..9f3a12b --- /dev/null +++ b/src/api/resources/wallet/types/Wallet.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface Wallet { + walletId: string; + walletAddress: string; + chainId: number; + nonce: number; + isActive: boolean; + projectId: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/api/resources/wallet/types/WalletWithTxCountAndBalance.ts b/src/api/resources/wallet/types/WalletWithTxCountAndBalance.ts new file mode 100644 index 0000000..34d93de --- /dev/null +++ b/src/api/resources/wallet/types/WalletWithTxCountAndBalance.ts @@ -0,0 +1,12 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as Syndicate from "../../.."; + +export interface WalletWithTxCountAndBalance extends Syndicate.wallet.Wallet { + /** Number of confirmed transactions submitted by this address */ + txCount?: number; + /** Balance in wei of this address's native gas token, as defined by chainId */ + balance?: string; +} diff --git a/src/api/resources/wallet/types/index.ts b/src/api/resources/wallet/types/index.ts new file mode 100644 index 0000000..bc5c57e --- /dev/null +++ b/src/api/resources/wallet/types/index.ts @@ -0,0 +1,13 @@ +export * from "./Wallet"; +export * from "./CreateWalletRequest"; +export * from "./CreateWalletResponse"; +export * from "./CreateWalletErrorBody"; +export * from "./WalletWithTxCountAndBalance"; +export * from "./RetireWalletRequest"; +export * from "./RetireWalletResponse"; +export * from "./TransactionResponse"; +export * from "./TransactionAttempt"; +export * from "./TransactionsByProjectResponse"; +export * from "./TransactionRequestsByProjectResponse"; +export * from "./TransactionStatus"; +export * from "./ProjectTransactionStatsResponse"; diff --git a/src/core/auth/BasicAuth.ts b/src/core/auth/BasicAuth.ts new file mode 100644 index 0000000..146df21 --- /dev/null +++ b/src/core/auth/BasicAuth.ts @@ -0,0 +1,31 @@ +import { Base64 } from "js-base64"; + +export interface BasicAuth { + username: string; + password: string; +} + +const BASIC_AUTH_HEADER_PREFIX = /^Basic /i; + +export const BasicAuth = { + toAuthorizationHeader: (basicAuth: BasicAuth | undefined): string | undefined => { + if (basicAuth == null) { + return undefined; + } + const token = Base64.encode(`${basicAuth.username}:${basicAuth.password}`); + return `Basic ${token}`; + }, + fromAuthorizationHeader: (header: string): BasicAuth => { + const credentials = header.replace(BASIC_AUTH_HEADER_PREFIX, ""); + const decoded = Base64.decode(credentials); + const [username, password] = decoded.split(":", 2); + + if (username == null || password == null) { + throw new Error("Invalid basic auth"); + } + return { + username, + password, + }; + }, +}; diff --git a/src/core/auth/BearerToken.ts b/src/core/auth/BearerToken.ts new file mode 100644 index 0000000..fe987fc --- /dev/null +++ b/src/core/auth/BearerToken.ts @@ -0,0 +1,15 @@ +export type BearerToken = string; + +const BEARER_AUTH_HEADER_PREFIX = /^Bearer /i; + +export const BearerToken = { + toAuthorizationHeader: (token: BearerToken | undefined): string | undefined => { + if (token == null) { + return undefined; + } + return `Bearer ${token}`; + }, + fromAuthorizationHeader: (header: string): BearerToken => { + return header.replace(BEARER_AUTH_HEADER_PREFIX, "").trim() as BearerToken; + }, +}; diff --git a/src/core/auth/index.ts b/src/core/auth/index.ts new file mode 100644 index 0000000..ee293b3 --- /dev/null +++ b/src/core/auth/index.ts @@ -0,0 +1,2 @@ +export { BasicAuth } from "./BasicAuth"; +export { BearerToken } from "./BearerToken"; diff --git a/src/core/fetcher/APIResponse.ts b/src/core/fetcher/APIResponse.ts new file mode 100644 index 0000000..ea838f3 --- /dev/null +++ b/src/core/fetcher/APIResponse.ts @@ -0,0 +1,11 @@ +export type APIResponse = SuccessfulResponse | FailedResponse; + +export interface SuccessfulResponse { + ok: true; + body: T; +} + +export interface FailedResponse { + ok: false; + error: T; +} diff --git a/src/core/fetcher/Fetcher.ts b/src/core/fetcher/Fetcher.ts new file mode 100644 index 0000000..6af0fb1 --- /dev/null +++ b/src/core/fetcher/Fetcher.ts @@ -0,0 +1,134 @@ +import { default as URLSearchParams } from "@ungap/url-search-params"; +import axios, { AxiosAdapter, AxiosError } from "axios"; +import { APIResponse } from "./APIResponse"; + +export type FetchFunction = (args: Fetcher.Args) => Promise>; + +export declare namespace Fetcher { + export interface Args { + url: string; + method: string; + contentType?: string; + headers?: Record; + queryParameters?: URLSearchParams; + body?: unknown; + timeoutMs?: number; + withCredentials?: boolean; + responseType?: "json" | "blob"; + adapter?: AxiosAdapter; + onUploadProgress?: (event: ProgressEvent) => void; + } + + export type Error = FailedStatusCodeError | NonJsonError | TimeoutError | UnknownError; + + export interface FailedStatusCodeError { + reason: "status-code"; + statusCode: number; + body: unknown; + } + + export interface NonJsonError { + reason: "non-json"; + statusCode: number; + rawBody: string; + } + + export interface TimeoutError { + reason: "timeout"; + } + + export interface UnknownError { + reason: "unknown"; + errorMessage: string; + } +} + +async function fetcherImpl(args: Fetcher.Args): Promise> { + const headers: Record = {}; + if (args.body !== undefined && args.contentType != null) { + headers["Content-Type"] = args.contentType; + } + + if (args.headers != null) { + for (const [key, value] of Object.entries(args.headers)) { + if (value != null) { + headers[key] = value; + } + } + } + + try { + const response = await axios({ + url: args.url, + params: args.queryParameters, + method: args.method, + headers, + data: args.body, + validateStatus: () => true, + transformResponse: (response) => response, + timeout: args.timeoutMs, + transitional: { + clarifyTimeoutError: true, + }, + withCredentials: args.withCredentials, + adapter: args.adapter, + onUploadProgress: args.onUploadProgress, + maxBodyLength: Infinity, + maxContentLength: Infinity, + responseType: args.responseType ?? "json", + }); + + let body: unknown; + if (args.responseType === "blob") { + body = response.data; + } else if (response.data != null && response.data.length > 0) { + try { + body = JSON.parse(response.data) ?? undefined; + } catch { + return { + ok: false, + error: { + reason: "non-json", + statusCode: response.status, + rawBody: response.data, + }, + }; + } + } + + if (response.status >= 200 && response.status < 400) { + return { + ok: true, + body: body as R, + }; + } else { + return { + ok: false, + error: { + reason: "status-code", + statusCode: response.status, + body, + }, + }; + } + } catch (error) { + if ((error as AxiosError).code === "ETIMEDOUT") { + return { + ok: false, + error: { + reason: "timeout", + }, + }; + } + + return { + ok: false, + error: { + reason: "unknown", + errorMessage: (error as AxiosError).message, + }, + }; + } +} + +export const fetcher: FetchFunction = fetcherImpl; diff --git a/src/core/fetcher/Supplier.ts b/src/core/fetcher/Supplier.ts new file mode 100644 index 0000000..867c931 --- /dev/null +++ b/src/core/fetcher/Supplier.ts @@ -0,0 +1,11 @@ +export type Supplier = T | Promise | (() => T | Promise); + +export const Supplier = { + get: async (supplier: Supplier): Promise => { + if (typeof supplier === "function") { + return (supplier as () => T)(); + } else { + return supplier; + } + }, +}; diff --git a/src/core/fetcher/index.ts b/src/core/fetcher/index.ts new file mode 100644 index 0000000..6becab2 --- /dev/null +++ b/src/core/fetcher/index.ts @@ -0,0 +1,4 @@ +export type { APIResponse } from "./APIResponse"; +export { fetcher } from "./Fetcher"; +export type { Fetcher, FetchFunction } from "./Fetcher"; +export { Supplier } from "./Supplier"; diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..6632dfb --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,3 @@ +export * from "./auth"; +export * from "./fetcher"; +export * as serialization from "./schemas"; diff --git a/src/core/schemas/Schema.ts b/src/core/schemas/Schema.ts new file mode 100644 index 0000000..3211fa4 --- /dev/null +++ b/src/core/schemas/Schema.ts @@ -0,0 +1,93 @@ +import { SchemaUtils } from "./builders"; +import { MaybePromise } from "./utils/MaybePromise"; + +export type Schema = BaseSchema & SchemaUtils; + +export type inferRaw = S extends Schema ? Raw : never; +export type inferParsed = S extends Schema ? Parsed : never; + +export interface BaseSchema { + parse: (raw: unknown, opts?: SchemaOptions) => MaybePromise>; + json: (parsed: unknown, opts?: SchemaOptions) => MaybePromise>; + getType: () => SchemaType | Promise; +} + +export const SchemaType = { + DATE: "date", + ENUM: "enum", + LIST: "list", + STRING_LITERAL: "stringLiteral", + OBJECT: "object", + ANY: "any", + BOOLEAN: "boolean", + NUMBER: "number", + STRING: "string", + UNKNOWN: "unknown", + RECORD: "record", + SET: "set", + UNION: "union", + UNDISCRIMINATED_UNION: "undiscriminatedUnion", + OPTIONAL: "optional", +} as const; +export type SchemaType = typeof SchemaType[keyof typeof SchemaType]; + +export type MaybeValid = Valid | Invalid; + +export interface Valid { + ok: true; + value: T; +} + +export interface Invalid { + ok: false; + errors: ValidationError[]; +} + +export interface ValidationError { + path: string[]; + message: string; +} + +export interface SchemaOptions { + /** + * how to handle unrecognized keys in objects + * + * @default "fail" + */ + unrecognizedObjectKeys?: "fail" | "passthrough" | "strip"; + + /** + * whether to fail when an unrecognized discriminant value is + * encountered in a union + * + * @default false + */ + allowUnrecognizedUnionMembers?: boolean; + + /** + * whether to fail when an unrecognized enum value is encountered + * + * @default false + */ + allowUnrecognizedEnumValues?: boolean; + + /** + * whether to allow data that doesn't conform to the schema. + * invalid data is passed through without transformation. + * + * when this is enabled, .parse() and .json() will always + * return `ok: true`. `.parseOrThrow()` and `.jsonOrThrow()` + * will never fail. + * + * @default false + */ + skipValidation?: boolean; + + /** + * each validation failure contains a "path" property, which is + * the breadcrumbs to the offending node in the JSON. you can supply + * a prefix that is prepended to all the errors' paths. this can be + * helpful for zurg's internal debug logging. + */ + breadcrumbsPrefix?: string[]; +} diff --git a/src/core/schemas/builders/date/date.ts b/src/core/schemas/builders/date/date.ts new file mode 100644 index 0000000..b70f24b --- /dev/null +++ b/src/core/schemas/builders/date/date.ts @@ -0,0 +1,65 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +// https://stackoverflow.com/questions/12756159/regex-and-iso8601-formatted-datetime +const ISO_8601_REGEX = + /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; + +export function date(): Schema { + const baseSchema: BaseSchema = { + parse: (raw, { breadcrumbsPrefix = [] } = {}) => { + if (typeof raw !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "string"), + }, + ], + }; + } + if (!ISO_8601_REGEX.test(raw)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "ISO 8601 date string"), + }, + ], + }; + } + return { + ok: true, + value: new Date(raw), + }; + }, + json: (date, { breadcrumbsPrefix = [] } = {}) => { + if (date instanceof Date) { + return { + ok: true, + value: date.toISOString(), + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(date, "Date object"), + }, + ], + }; + } + }, + getType: () => SchemaType.DATE, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/date/index.ts b/src/core/schemas/builders/date/index.ts new file mode 100644 index 0000000..187b290 --- /dev/null +++ b/src/core/schemas/builders/date/index.ts @@ -0,0 +1 @@ +export { date } from "./date"; diff --git a/src/core/schemas/builders/enum/enum.ts b/src/core/schemas/builders/enum/enum.ts new file mode 100644 index 0000000..c1e24d6 --- /dev/null +++ b/src/core/schemas/builders/enum/enum.ts @@ -0,0 +1,43 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function enum_(values: E): Schema { + const validValues = new Set(values); + + const schemaCreator = createIdentitySchemaCreator( + SchemaType.ENUM, + (value, { allowUnrecognizedEnumValues, breadcrumbsPrefix = [] } = {}) => { + if (typeof value !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "string"), + }, + ], + }; + } + + if (!validValues.has(value) && !allowUnrecognizedEnumValues) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "enum"), + }, + ], + }; + } + + return { + ok: true, + value: value as U, + }; + } + ); + + return schemaCreator(); +} diff --git a/src/core/schemas/builders/enum/index.ts b/src/core/schemas/builders/enum/index.ts new file mode 100644 index 0000000..fe6faed --- /dev/null +++ b/src/core/schemas/builders/enum/index.ts @@ -0,0 +1 @@ +export { enum_ } from "./enum"; diff --git a/src/core/schemas/builders/index.ts b/src/core/schemas/builders/index.ts new file mode 100644 index 0000000..050cd2c --- /dev/null +++ b/src/core/schemas/builders/index.ts @@ -0,0 +1,13 @@ +export * from "./date"; +export * from "./enum"; +export * from "./lazy"; +export * from "./list"; +export * from "./literals"; +export * from "./object"; +export * from "./object-like"; +export * from "./primitives"; +export * from "./record"; +export * from "./schema-utils"; +export * from "./set"; +export * from "./undiscriminated-union"; +export * from "./union"; diff --git a/src/core/schemas/builders/lazy/index.ts b/src/core/schemas/builders/lazy/index.ts new file mode 100644 index 0000000..77420fb --- /dev/null +++ b/src/core/schemas/builders/lazy/index.ts @@ -0,0 +1,3 @@ +export { lazy } from "./lazy"; +export type { SchemaGetter } from "./lazy"; +export { lazyObject } from "./lazyObject"; diff --git a/src/core/schemas/builders/lazy/lazy.ts b/src/core/schemas/builders/lazy/lazy.ts new file mode 100644 index 0000000..a665472 --- /dev/null +++ b/src/core/schemas/builders/lazy/lazy.ts @@ -0,0 +1,34 @@ +import { BaseSchema, Schema } from "../../Schema"; +import { getSchemaUtils } from "../schema-utils"; + +export type SchemaGetter> = () => SchemaType | Promise; + +export function lazy(getter: SchemaGetter>): Schema { + const baseSchema = constructLazyBaseSchema(getter); + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} + +export function constructLazyBaseSchema( + getter: SchemaGetter> +): BaseSchema { + return { + parse: async (raw, opts) => (await getMemoizedSchema(getter)).parse(raw, opts), + json: async (parsed, opts) => (await getMemoizedSchema(getter)).json(parsed, opts), + getType: async () => (await getMemoizedSchema(getter)).getType(), + }; +} + +type MemoizedGetter> = SchemaGetter & { __zurg_memoized?: SchemaType }; + +export async function getMemoizedSchema>( + getter: SchemaGetter +): Promise { + const castedGetter = getter as MemoizedGetter; + if (castedGetter.__zurg_memoized == null) { + castedGetter.__zurg_memoized = await getter(); + } + return castedGetter.__zurg_memoized; +} diff --git a/src/core/schemas/builders/lazy/lazyObject.ts b/src/core/schemas/builders/lazy/lazyObject.ts new file mode 100644 index 0000000..e48c016 --- /dev/null +++ b/src/core/schemas/builders/lazy/lazyObject.ts @@ -0,0 +1,20 @@ +import { getObjectUtils } from "../object"; +import { getObjectLikeUtils } from "../object-like"; +import { BaseObjectSchema, ObjectSchema } from "../object/types"; +import { getSchemaUtils } from "../schema-utils"; +import { constructLazyBaseSchema, getMemoizedSchema, SchemaGetter } from "./lazy"; + +export function lazyObject(getter: SchemaGetter>): ObjectSchema { + const baseSchema: BaseObjectSchema = { + ...constructLazyBaseSchema(getter), + _getRawProperties: async () => (await getMemoizedSchema(getter))._getRawProperties(), + _getParsedProperties: async () => (await getMemoizedSchema(getter))._getParsedProperties(), + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/list/index.ts b/src/core/schemas/builders/list/index.ts new file mode 100644 index 0000000..25f4bcc --- /dev/null +++ b/src/core/schemas/builders/list/index.ts @@ -0,0 +1 @@ +export { list } from "./list"; diff --git a/src/core/schemas/builders/list/list.ts b/src/core/schemas/builders/list/list.ts new file mode 100644 index 0000000..b333321 --- /dev/null +++ b/src/core/schemas/builders/list/list.ts @@ -0,0 +1,74 @@ +import { BaseSchema, MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { MaybePromise } from "../../utils/MaybePromise"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +export function list(schema: Schema): Schema { + const baseSchema: BaseSchema = { + parse: async (raw, opts) => + validateAndTransformArray(raw, (item, index) => + schema.parse(item, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `[${index}]`], + }) + ), + json: (parsed, opts) => + validateAndTransformArray(parsed, (item, index) => + schema.json(item, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `[${index}]`], + }) + ), + getType: () => SchemaType.LIST, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +async function validateAndTransformArray( + value: unknown, + transformItem: (item: Raw, index: number) => MaybePromise> +): Promise> { + if (!Array.isArray(value)) { + return { + ok: false, + errors: [ + { + message: getErrorMessageForIncorrectType(value, "list"), + path: [], + }, + ], + }; + } + + const maybeValidItems = await Promise.all(value.map((item, index) => transformItem(item, index))); + + return maybeValidItems.reduce>( + (acc, item) => { + if (acc.ok && item.ok) { + return { + ok: true, + value: [...acc.value, item.value], + }; + } + + const errors: ValidationError[] = []; + if (!acc.ok) { + errors.push(...acc.errors); + } + if (!item.ok) { + errors.push(...item.errors); + } + + return { + ok: false, + errors, + }; + }, + { ok: true, value: [] } + ); +} diff --git a/src/core/schemas/builders/literals/index.ts b/src/core/schemas/builders/literals/index.ts new file mode 100644 index 0000000..a4cd05c --- /dev/null +++ b/src/core/schemas/builders/literals/index.ts @@ -0,0 +1 @@ +export { stringLiteral } from "./stringLiteral"; diff --git a/src/core/schemas/builders/literals/stringLiteral.ts b/src/core/schemas/builders/literals/stringLiteral.ts new file mode 100644 index 0000000..3939b76 --- /dev/null +++ b/src/core/schemas/builders/literals/stringLiteral.ts @@ -0,0 +1,29 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function stringLiteral(literal: V): Schema { + const schemaCreator = createIdentitySchemaCreator( + SchemaType.STRING_LITERAL, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (value === literal) { + return { + ok: true, + value: literal, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, `"${literal}"`), + }, + ], + }; + } + } + ); + + return schemaCreator(); +} diff --git a/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/src/core/schemas/builders/object-like/getObjectLikeUtils.ts new file mode 100644 index 0000000..270ea17 --- /dev/null +++ b/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -0,0 +1,79 @@ +import { BaseSchema } from "../../Schema"; +import { filterObject } from "../../utils/filterObject"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { getSchemaUtils } from "../schema-utils"; +import { ObjectLikeSchema, ObjectLikeUtils } from "./types"; + +export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { + return { + withParsedProperties: (properties) => withParsedProperties(schema, properties), + }; +} + +/** + * object-like utils are defined in one file to resolve issues with circular imports + */ + +export function withParsedProperties( + objectLike: BaseSchema, + properties: { [K in keyof Properties]: Properties[K] | ((parsed: ParsedObjectShape) => Properties[K]) } +): ObjectLikeSchema { + const objectSchema: BaseSchema = { + parse: async (raw, opts) => { + const parsedObject = await objectLike.parse(raw, opts); + if (!parsedObject.ok) { + return parsedObject; + } + + const additionalProperties = Object.entries(properties).reduce>( + (processed, [key, value]) => { + return { + ...processed, + [key]: typeof value === "function" ? value(parsedObject.value) : value, + }; + }, + {} + ); + + return { + ok: true, + value: { + ...parsedObject.value, + ...(additionalProperties as Properties), + }, + }; + }, + + json: (parsed, opts) => { + if (!isPlainObject(parsed)) { + return { + ok: false, + errors: [ + { + path: opts?.breadcrumbsPrefix ?? [], + message: getErrorMessageForIncorrectType(parsed, "object"), + }, + ], + }; + } + + // strip out added properties + const addedPropertyKeys = new Set(Object.keys(properties)); + const parsedWithoutAddedProperties = filterObject( + parsed, + Object.keys(parsed).filter((key) => !addedPropertyKeys.has(key)) + ); + + return objectLike.json(parsedWithoutAddedProperties as ParsedObjectShape, opts); + }, + + getType: () => objectLike.getType(), + }; + + return { + ...objectSchema, + ...getSchemaUtils(objectSchema), + ...getObjectLikeUtils(objectSchema), + }; +} diff --git a/src/core/schemas/builders/object-like/index.ts b/src/core/schemas/builders/object-like/index.ts new file mode 100644 index 0000000..c342e72 --- /dev/null +++ b/src/core/schemas/builders/object-like/index.ts @@ -0,0 +1,2 @@ +export { getObjectLikeUtils, withParsedProperties } from "./getObjectLikeUtils"; +export type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; diff --git a/src/core/schemas/builders/object-like/types.ts b/src/core/schemas/builders/object-like/types.ts new file mode 100644 index 0000000..75b3698 --- /dev/null +++ b/src/core/schemas/builders/object-like/types.ts @@ -0,0 +1,11 @@ +import { BaseSchema, Schema } from "../../Schema"; + +export type ObjectLikeSchema = Schema & + BaseSchema & + ObjectLikeUtils; + +export interface ObjectLikeUtils { + withParsedProperties: >(properties: { + [K in keyof T]: T[K] | ((parsed: Parsed) => T[K]); + }) => ObjectLikeSchema; +} diff --git a/src/core/schemas/builders/object/index.ts b/src/core/schemas/builders/object/index.ts new file mode 100644 index 0000000..e6db5b5 --- /dev/null +++ b/src/core/schemas/builders/object/index.ts @@ -0,0 +1,17 @@ +export { getObjectUtils, object } from "./object"; +export { isProperty, property } from "./property"; +export type { Property } from "./property"; +export type { + BaseObjectSchema, + inferObjectSchemaFromPropertySchemas, + inferParsedObject, + inferParsedObjectFromPropertySchemas, + inferParsedPropertySchema, + inferRawKey, + inferRawObject, + inferRawObjectFromPropertySchemas, + inferRawPropertySchema, + ObjectSchema, + ObjectUtils, + PropertySchemas, +} from "./types"; diff --git a/src/core/schemas/builders/object/object.ts b/src/core/schemas/builders/object/object.ts new file mode 100644 index 0000000..4abadfb --- /dev/null +++ b/src/core/schemas/builders/object/object.ts @@ -0,0 +1,333 @@ +import { MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { entries } from "../../utils/entries"; +import { filterObject } from "../../utils/filterObject"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { keys } from "../../utils/keys"; +import { MaybePromise } from "../../utils/MaybePromise"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { partition } from "../../utils/partition"; +import { getObjectLikeUtils } from "../object-like"; +import { getSchemaUtils } from "../schema-utils"; +import { isProperty } from "./property"; +import { + BaseObjectSchema, + inferObjectSchemaFromPropertySchemas, + inferParsedObjectFromPropertySchemas, + inferRawObjectFromPropertySchemas, + ObjectSchema, + ObjectUtils, + PropertySchemas, +} from "./types"; + +interface ObjectPropertyWithRawKey { + rawKey: string; + parsedKey: string; + valueSchema: Schema; +} + +export function object>( + schemas: T +): inferObjectSchemaFromPropertySchemas { + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Promise.resolve( + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[] + ), + _getParsedProperties: () => + Promise.resolve(keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[]), + + parse: async (raw, opts) => { + const rawKeyToProperty: Record = {}; + const requiredKeys: string[] = []; + + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; + + const property: ObjectPropertyWithRawKey = { + rawKey, + parsedKey: parsedKey as string, + valueSchema, + }; + + rawKeyToProperty[rawKey] = property; + + if (await isSchemaRequired(valueSchema)) { + requiredKeys.push(rawKey); + } + } + + return validateAndTransformObject({ + value: raw, + requiredKeys, + getProperty: (rawKey) => { + const property = rawKeyToProperty[rawKey]; + if (property == null) { + return undefined; + } + return { + transformedKey: property.parsedKey, + transform: (propertyValue) => + property.valueSchema.parse(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], + }), + }; + }, + unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, + skipValidation: opts?.skipValidation, + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + + json: async (parsed, opts) => { + const requiredKeys: string[] = []; + + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; + + if (await isSchemaRequired(valueSchema)) { + requiredKeys.push(parsedKey as string); + } + } + + return validateAndTransformObject({ + value: parsed, + requiredKeys, + getProperty: ( + parsedKey + ): + | { transformedKey: string; transform: (propertyValue: unknown) => MaybePromise> } + | undefined => { + const property = schemas[parsedKey as keyof T]; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (property == null) { + return undefined; + } + + if (isProperty(property)) { + return { + transformedKey: property.rawKey, + transform: (propertyValue) => + property.valueSchema.json(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], + }), + }; + } else { + return { + transformedKey: parsedKey, + transform: (propertyValue) => + property.json(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], + }), + }; + } + }, + unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, + skipValidation: opts?.skipValidation, + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + + getType: () => SchemaType.OBJECT, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; +} + +async function validateAndTransformObject({ + value, + requiredKeys, + getProperty, + unrecognizedObjectKeys = "fail", + skipValidation = false, + breadcrumbsPrefix = [], +}: { + value: unknown; + requiredKeys: string[]; + getProperty: ( + preTransformedKey: string + ) => { transformedKey: string; transform: (propertyValue: unknown) => MaybePromise> } | undefined; + unrecognizedObjectKeys: "fail" | "passthrough" | "strip" | undefined; + skipValidation: boolean | undefined; + breadcrumbsPrefix: string[] | undefined; +}): Promise> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + const missingRequiredKeys = new Set(requiredKeys); + const errors: ValidationError[] = []; + const transformed: Record = {}; + + for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + const property = getProperty(preTransformedKey); + + if (property != null) { + missingRequiredKeys.delete(preTransformedKey); + + const value = await property.transform(preTransformedItemValue); + if (value.ok) { + transformed[property.transformedKey] = value.value; + } else { + transformed[preTransformedKey] = preTransformedItemValue; + errors.push(...value.errors); + } + } else { + switch (unrecognizedObjectKeys) { + case "fail": + errors.push({ + path: [...breadcrumbsPrefix, preTransformedKey], + message: `Unexpected key "${preTransformedKey}"`, + }); + break; + case "strip": + break; + case "passthrough": + transformed[preTransformedKey] = preTransformedItemValue; + break; + } + } + } + + errors.push( + ...requiredKeys + .filter((key) => missingRequiredKeys.has(key)) + .map((key) => ({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + })) + ); + + if (errors.length === 0 || skipValidation) { + return { + ok: true, + value: transformed as Transformed, + }; + } else { + return { + ok: false, + errors, + }; + } +} + +export function getObjectUtils(schema: BaseObjectSchema): ObjectUtils { + return { + extend: (extension: ObjectSchema) => { + const baseSchema: BaseObjectSchema = { + _getParsedProperties: async () => [ + ...(await schema._getParsedProperties()), + ...(await extension._getParsedProperties()), + ], + _getRawProperties: async () => [ + ...(await schema._getRawProperties()), + ...(await extension._getRawProperties()), + ], + parse: async (raw, opts) => { + return validateAndTransformExtendedObject({ + extensionKeys: await extension._getRawProperties(), + value: raw, + transformBase: (rawBase) => schema.parse(rawBase, opts), + transformExtension: (rawExtension) => extension.parse(rawExtension, opts), + }); + }, + json: async (parsed, opts) => { + return validateAndTransformExtendedObject({ + extensionKeys: await extension._getParsedProperties(), + value: parsed, + transformBase: (parsedBase) => schema.json(parsedBase, opts), + transformExtension: (parsedExtension) => extension.json(parsedExtension, opts), + }); + }, + getType: () => SchemaType.OBJECT, + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; + }, + }; +} + +async function validateAndTransformExtendedObject({ + extensionKeys, + value, + transformBase, + transformExtension, +}: { + extensionKeys: (keyof PreTransformedExtension)[]; + value: unknown; + transformBase: (value: unknown) => MaybePromise>; + transformExtension: (value: unknown) => MaybePromise>; +}): Promise> { + const extensionPropertiesSet = new Set(extensionKeys); + const [extensionProperties, baseProperties] = partition(keys(value), (key) => + extensionPropertiesSet.has(key as keyof PreTransformedExtension) + ); + + const transformedBase = await transformBase(filterObject(value, baseProperties)); + const transformedExtension = await transformExtension(filterObject(value, extensionProperties)); + + if (transformedBase.ok && transformedExtension.ok) { + return { + ok: true, + value: { + ...transformedBase.value, + ...transformedExtension.value, + }, + }; + } else { + return { + ok: false, + errors: [ + ...(transformedBase.ok ? [] : transformedBase.errors), + ...(transformedExtension.ok ? [] : transformedExtension.errors), + ], + }; + } +} + +async function isSchemaRequired(schema: Schema): Promise { + return !(await isSchemaOptional(schema)); +} + +async function isSchemaOptional(schema: Schema): Promise { + switch (await schema.getType()) { + case SchemaType.ANY: + case SchemaType.UNKNOWN: + case SchemaType.OPTIONAL: + return true; + default: + return false; + } +} diff --git a/src/core/schemas/builders/object/property.ts b/src/core/schemas/builders/object/property.ts new file mode 100644 index 0000000..d245c4b --- /dev/null +++ b/src/core/schemas/builders/object/property.ts @@ -0,0 +1,23 @@ +import { Schema } from "../../Schema"; + +export function property( + rawKey: RawKey, + valueSchema: Schema +): Property { + return { + rawKey, + valueSchema, + isProperty: true, + }; +} + +export interface Property { + rawKey: RawKey; + valueSchema: Schema; + isProperty: true; +} + +export function isProperty>(maybeProperty: unknown): maybeProperty is O { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return (maybeProperty as O).isProperty; +} diff --git a/src/core/schemas/builders/object/types.ts b/src/core/schemas/builders/object/types.ts new file mode 100644 index 0000000..17cff4f --- /dev/null +++ b/src/core/schemas/builders/object/types.ts @@ -0,0 +1,72 @@ +import { BaseSchema, inferParsed, inferRaw, Schema } from "../../Schema"; +import { addQuestionMarksToNullableProperties } from "../../utils/addQuestionMarksToNullableProperties"; +import { ObjectLikeUtils } from "../object-like"; +import { SchemaUtils } from "../schema-utils"; +import { Property } from "./property"; + +export type ObjectSchema = BaseObjectSchema & + ObjectLikeUtils & + ObjectUtils & + SchemaUtils; + +export interface BaseObjectSchema extends BaseSchema { + _getRawProperties: () => Promise<(keyof Raw)[]>; + _getParsedProperties: () => Promise<(keyof Parsed)[]>; +} + +export interface ObjectUtils { + extend: ( + schemas: ObjectSchema + ) => ObjectSchema; +} + +export type inferRawObject> = O extends ObjectSchema ? Raw : never; + +export type inferParsedObject> = O extends ObjectSchema + ? Parsed + : never; + +export type inferObjectSchemaFromPropertySchemas> = ObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas +>; + +export type inferRawObjectFromPropertySchemas> = + addQuestionMarksToNullableProperties<{ + [ParsedKey in keyof T as inferRawKey]: inferRawPropertySchema; + }>; + +export type inferParsedObjectFromPropertySchemas> = + addQuestionMarksToNullableProperties<{ + [K in keyof T]: inferParsedPropertySchema; + }>; + +export type PropertySchemas = Record< + ParsedKeys, + Property | Schema +>; + +export type inferRawPropertySchema

| Schema> = P extends Property< + any, + infer Raw, + any +> + ? Raw + : P extends Schema + ? inferRaw

+ : never; + +export type inferParsedPropertySchema

| Schema> = P extends Property< + any, + any, + infer Parsed +> + ? Parsed + : P extends Schema + ? inferParsed

+ : never; + +export type inferRawKey< + ParsedKey extends string | number | symbol, + P extends Property | Schema +> = P extends Property ? Raw : ParsedKey; diff --git a/src/core/schemas/builders/primitives/any.ts b/src/core/schemas/builders/primitives/any.ts new file mode 100644 index 0000000..fcaeb04 --- /dev/null +++ b/src/core/schemas/builders/primitives/any.ts @@ -0,0 +1,4 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; + +export const any = createIdentitySchemaCreator(SchemaType.ANY, (value) => ({ ok: true, value })); diff --git a/src/core/schemas/builders/primitives/boolean.ts b/src/core/schemas/builders/primitives/boolean.ts new file mode 100644 index 0000000..fad6056 --- /dev/null +++ b/src/core/schemas/builders/primitives/boolean.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const boolean = createIdentitySchemaCreator( + SchemaType.BOOLEAN, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "boolean") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "boolean"), + }, + ], + }; + } + } +); diff --git a/src/core/schemas/builders/primitives/index.ts b/src/core/schemas/builders/primitives/index.ts new file mode 100644 index 0000000..788f941 --- /dev/null +++ b/src/core/schemas/builders/primitives/index.ts @@ -0,0 +1,5 @@ +export { any } from "./any"; +export { boolean } from "./boolean"; +export { number } from "./number"; +export { string } from "./string"; +export { unknown } from "./unknown"; diff --git a/src/core/schemas/builders/primitives/number.ts b/src/core/schemas/builders/primitives/number.ts new file mode 100644 index 0000000..c268945 --- /dev/null +++ b/src/core/schemas/builders/primitives/number.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const number = createIdentitySchemaCreator( + SchemaType.NUMBER, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "number") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "number"), + }, + ], + }; + } + } +); diff --git a/src/core/schemas/builders/primitives/string.ts b/src/core/schemas/builders/primitives/string.ts new file mode 100644 index 0000000..949f1f2 --- /dev/null +++ b/src/core/schemas/builders/primitives/string.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const string = createIdentitySchemaCreator( + SchemaType.STRING, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "string") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "string"), + }, + ], + }; + } + } +); diff --git a/src/core/schemas/builders/primitives/unknown.ts b/src/core/schemas/builders/primitives/unknown.ts new file mode 100644 index 0000000..4d52495 --- /dev/null +++ b/src/core/schemas/builders/primitives/unknown.ts @@ -0,0 +1,4 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; + +export const unknown = createIdentitySchemaCreator(SchemaType.UNKNOWN, (value) => ({ ok: true, value })); diff --git a/src/core/schemas/builders/record/index.ts b/src/core/schemas/builders/record/index.ts new file mode 100644 index 0000000..82e25c5 --- /dev/null +++ b/src/core/schemas/builders/record/index.ts @@ -0,0 +1,2 @@ +export { record } from "./record"; +export type { BaseRecordSchema, RecordSchema } from "./types"; diff --git a/src/core/schemas/builders/record/record.ts b/src/core/schemas/builders/record/record.ts new file mode 100644 index 0000000..ac1cd22 --- /dev/null +++ b/src/core/schemas/builders/record/record.ts @@ -0,0 +1,131 @@ +import { MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { entries } from "../../utils/entries"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { MaybePromise } from "../../utils/MaybePromise"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; +import { BaseRecordSchema, RecordSchema } from "./types"; + +export function record( + keySchema: Schema, + valueSchema: Schema +): RecordSchema { + const baseSchema: BaseRecordSchema = { + parse: async (raw, opts) => { + return validateAndTransformRecord({ + value: raw, + isKeyNumeric: (await keySchema.getType()) === SchemaType.NUMBER, + transformKey: (key) => + keySchema.parse(key, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key} (key)`], + }), + transformValue: (value, key) => + valueSchema.parse(value, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key}`], + }), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + json: async (parsed, opts) => { + return validateAndTransformRecord({ + value: parsed, + isKeyNumeric: (await keySchema.getType()) === SchemaType.NUMBER, + transformKey: (key) => + keySchema.json(key, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key} (key)`], + }), + transformValue: (value, key) => + valueSchema.json(value, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key}`], + }), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + getType: () => SchemaType.RECORD, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +async function validateAndTransformRecord({ + value, + isKeyNumeric, + transformKey, + transformValue, + breadcrumbsPrefix = [], +}: { + value: unknown; + isKeyNumeric: boolean; + transformKey: (key: string | number) => MaybePromise>; + transformValue: (value: unknown, key: string | number) => MaybePromise>; + breadcrumbsPrefix: string[] | undefined; +}): Promise>> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + return entries(value).reduce>>>( + async (accPromise, [stringKey, value]) => { + // skip nullish keys + if (value == null) { + return accPromise; + } + + const acc = await accPromise; + + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!isNaN(numberKey)) { + key = numberKey; + } + } + const transformedKey = await transformKey(key); + + const transformedValue = await transformValue(value, key); + + if (acc.ok && transformedKey.ok && transformedValue.ok) { + return { + ok: true, + value: { + ...acc.value, + [transformedKey.value]: transformedValue.value, + }, + }; + } + + const errors: ValidationError[] = []; + if (!acc.ok) { + errors.push(...acc.errors); + } + if (!transformedKey.ok) { + errors.push(...transformedKey.errors); + } + if (!transformedValue.ok) { + errors.push(...transformedValue.errors); + } + + return { + ok: false, + errors, + }; + }, + Promise.resolve({ ok: true, value: {} as Record }) + ); +} diff --git a/src/core/schemas/builders/record/types.ts b/src/core/schemas/builders/record/types.ts new file mode 100644 index 0000000..eb82cc7 --- /dev/null +++ b/src/core/schemas/builders/record/types.ts @@ -0,0 +1,17 @@ +import { BaseSchema } from "../../Schema"; +import { SchemaUtils } from "../schema-utils"; + +export type RecordSchema< + RawKey extends string | number, + RawValue, + ParsedKey extends string | number, + ParsedValue +> = BaseRecordSchema & + SchemaUtils, Record>; + +export type BaseRecordSchema< + RawKey extends string | number, + RawValue, + ParsedKey extends string | number, + ParsedValue +> = BaseSchema, Record>; diff --git a/src/core/schemas/builders/schema-utils/JsonError.ts b/src/core/schemas/builders/schema-utils/JsonError.ts new file mode 100644 index 0000000..2b89ca0 --- /dev/null +++ b/src/core/schemas/builders/schema-utils/JsonError.ts @@ -0,0 +1,9 @@ +import { ValidationError } from "../../Schema"; +import { stringifyValidationError } from "./stringifyValidationErrors"; + +export class JsonError extends Error { + constructor(public readonly errors: ValidationError[]) { + super(errors.map(stringifyValidationError).join("; ")); + Object.setPrototypeOf(this, JsonError.prototype); + } +} diff --git a/src/core/schemas/builders/schema-utils/ParseError.ts b/src/core/schemas/builders/schema-utils/ParseError.ts new file mode 100644 index 0000000..d056eb4 --- /dev/null +++ b/src/core/schemas/builders/schema-utils/ParseError.ts @@ -0,0 +1,9 @@ +import { ValidationError } from "../../Schema"; +import { stringifyValidationError } from "./stringifyValidationErrors"; + +export class ParseError extends Error { + constructor(public readonly errors: ValidationError[]) { + super(errors.map(stringifyValidationError).join("; ")); + Object.setPrototypeOf(this, ParseError.prototype); + } +} diff --git a/src/core/schemas/builders/schema-utils/getSchemaUtils.ts b/src/core/schemas/builders/schema-utils/getSchemaUtils.ts new file mode 100644 index 0000000..0c0d379 --- /dev/null +++ b/src/core/schemas/builders/schema-utils/getSchemaUtils.ts @@ -0,0 +1,99 @@ +import { BaseSchema, Schema, SchemaOptions, SchemaType } from "../../Schema"; +import { JsonError } from "./JsonError"; +import { ParseError } from "./ParseError"; + +export interface SchemaUtils { + optional: () => Schema; + transform: (transformer: SchemaTransformer) => Schema; + parseOrThrow: (raw: unknown, opts?: SchemaOptions) => Promise; + jsonOrThrow: (raw: unknown, opts?: SchemaOptions) => Promise; +} + +export interface SchemaTransformer { + transform: (parsed: Parsed) => Transformed; + untransform: (transformed: any) => Parsed; +} + +export function getSchemaUtils(schema: BaseSchema): SchemaUtils { + return { + optional: () => optional(schema), + transform: (transformer) => transform(schema, transformer), + parseOrThrow: async (raw, opts) => { + const parsed = await schema.parse(raw, opts); + if (parsed.ok) { + return parsed.value; + } + throw new ParseError(parsed.errors); + }, + jsonOrThrow: async (parsed, opts) => { + const raw = await schema.json(parsed, opts); + if (raw.ok) { + return raw.value; + } + throw new JsonError(raw.errors); + }, + }; +} + +/** + * schema utils are defined in one file to resolve issues with circular imports + */ + +export function optional( + schema: BaseSchema +): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => { + if (raw == null) { + return { + ok: true, + value: undefined, + }; + } + return schema.parse(raw, opts); + }, + json: (parsed, opts) => { + if (parsed == null) { + return { + ok: true, + value: null, + }; + } + return schema.json(parsed, opts); + }, + getType: () => SchemaType.OPTIONAL, + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} + +export function transform( + schema: BaseSchema, + transformer: SchemaTransformer +): Schema { + const baseSchema: BaseSchema = { + parse: async (raw, opts) => { + const parsed = await schema.parse(raw, opts); + if (!parsed.ok) { + return parsed; + } + return { + ok: true, + value: transformer.transform(parsed.value), + }; + }, + json: async (transformed, opts) => { + const parsed = await transformer.untransform(transformed); + return schema.json(parsed, opts); + }, + getType: () => schema.getType(), + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/schema-utils/index.ts b/src/core/schemas/builders/schema-utils/index.ts new file mode 100644 index 0000000..aa04e05 --- /dev/null +++ b/src/core/schemas/builders/schema-utils/index.ts @@ -0,0 +1,4 @@ +export { getSchemaUtils, optional, transform } from "./getSchemaUtils"; +export type { SchemaUtils } from "./getSchemaUtils"; +export { JsonError } from "./JsonError"; +export { ParseError } from "./ParseError"; diff --git a/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts b/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts new file mode 100644 index 0000000..4160f0a --- /dev/null +++ b/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts @@ -0,0 +1,8 @@ +import { ValidationError } from "../../Schema"; + +export function stringifyValidationError(error: ValidationError): string { + if (error.path.length === 0) { + return error.message; + } + return `${error.path.join(" -> ")}: ${error.message}`; +} diff --git a/src/core/schemas/builders/set/index.ts b/src/core/schemas/builders/set/index.ts new file mode 100644 index 0000000..f3310e8 --- /dev/null +++ b/src/core/schemas/builders/set/index.ts @@ -0,0 +1 @@ +export { set } from "./set"; diff --git a/src/core/schemas/builders/set/set.ts b/src/core/schemas/builders/set/set.ts new file mode 100644 index 0000000..3113bcb --- /dev/null +++ b/src/core/schemas/builders/set/set.ts @@ -0,0 +1,43 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { list } from "../list"; +import { getSchemaUtils } from "../schema-utils"; + +export function set(schema: Schema): Schema> { + const listSchema = list(schema); + const baseSchema: BaseSchema> = { + parse: async (raw, opts) => { + const parsedList = await listSchema.parse(raw, opts); + if (parsedList.ok) { + return { + ok: true, + value: new Set(parsedList.value), + }; + } else { + return parsedList; + } + }, + json: async (parsed, opts) => { + if (!(parsed instanceof Set)) { + return { + ok: false, + errors: [ + { + path: opts?.breadcrumbsPrefix ?? [], + message: getErrorMessageForIncorrectType(parsed, "Set"), + }, + ], + }; + } + const jsonList = await listSchema.json([...parsed], opts); + return jsonList; + }, + getType: () => SchemaType.SET, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/undiscriminated-union/index.ts b/src/core/schemas/builders/undiscriminated-union/index.ts new file mode 100644 index 0000000..75b71cb --- /dev/null +++ b/src/core/schemas/builders/undiscriminated-union/index.ts @@ -0,0 +1,6 @@ +export type { + inferParsedUnidiscriminatedUnionSchema, + inferRawUnidiscriminatedUnionSchema, + UndiscriminatedUnionSchema, +} from "./types"; +export { undiscriminatedUnion } from "./undiscriminatedUnion"; diff --git a/src/core/schemas/builders/undiscriminated-union/types.ts b/src/core/schemas/builders/undiscriminated-union/types.ts new file mode 100644 index 0000000..43e7108 --- /dev/null +++ b/src/core/schemas/builders/undiscriminated-union/types.ts @@ -0,0 +1,10 @@ +import { inferParsed, inferRaw, Schema } from "../../Schema"; + +export type UndiscriminatedUnionSchema = Schema< + inferRawUnidiscriminatedUnionSchema, + inferParsedUnidiscriminatedUnionSchema +>; + +export type inferRawUnidiscriminatedUnionSchema = inferRaw; + +export type inferParsedUnidiscriminatedUnionSchema = inferParsed; diff --git a/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts b/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts new file mode 100644 index 0000000..771dc6a --- /dev/null +++ b/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts @@ -0,0 +1,61 @@ +import { BaseSchema, MaybeValid, Schema, SchemaOptions, SchemaType, ValidationError } from "../../Schema"; +import { MaybePromise } from "../../utils/MaybePromise"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; +import { inferParsedUnidiscriminatedUnionSchema, inferRawUnidiscriminatedUnionSchema } from "./types"; + +export function undiscriminatedUnion, ...Schema[]]>( + schemas: Schemas +): Schema, inferParsedUnidiscriminatedUnionSchema> { + const baseSchema: BaseSchema< + inferRawUnidiscriminatedUnionSchema, + inferParsedUnidiscriminatedUnionSchema + > = { + parse: async (raw, opts) => { + return validateAndTransformUndiscriminatedUnion>( + (schema, opts) => schema.parse(raw, opts), + schemas, + opts + ); + }, + json: async (parsed, opts) => { + return validateAndTransformUndiscriminatedUnion>( + (schema, opts) => schema.json(parsed, opts), + schemas, + opts + ); + }, + getType: () => SchemaType.UNDISCRIMINATED_UNION, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +async function validateAndTransformUndiscriminatedUnion( + transform: (schema: Schema, opts: SchemaOptions) => MaybePromise>, + schemas: Schema[], + opts: SchemaOptions | undefined +): Promise> { + const errors: ValidationError[] = []; + for (const [index, schema] of schemas.entries()) { + const transformed = await transform(schema, { ...opts, skipValidation: false }); + if (transformed.ok) { + return transformed; + } else { + for (const error of transformed.errors) { + errors.push({ + path: error.path, + message: `[Variant ${index}] ${error.message}`, + }); + } + } + } + + return { + ok: false, + errors, + }; +} diff --git a/src/core/schemas/builders/union/discriminant.ts b/src/core/schemas/builders/union/discriminant.ts new file mode 100644 index 0000000..55065bc --- /dev/null +++ b/src/core/schemas/builders/union/discriminant.ts @@ -0,0 +1,14 @@ +export function discriminant( + parsedDiscriminant: ParsedDiscriminant, + rawDiscriminant: RawDiscriminant +): Discriminant { + return { + parsedDiscriminant, + rawDiscriminant, + }; +} + +export interface Discriminant { + parsedDiscriminant: ParsedDiscriminant; + rawDiscriminant: RawDiscriminant; +} diff --git a/src/core/schemas/builders/union/index.ts b/src/core/schemas/builders/union/index.ts new file mode 100644 index 0000000..85fc008 --- /dev/null +++ b/src/core/schemas/builders/union/index.ts @@ -0,0 +1,10 @@ +export { discriminant } from "./discriminant"; +export type { Discriminant } from "./discriminant"; +export type { + inferParsedDiscriminant, + inferParsedUnion, + inferRawDiscriminant, + inferRawUnion, + UnionSubtypes, +} from "./types"; +export { union } from "./union"; diff --git a/src/core/schemas/builders/union/types.ts b/src/core/schemas/builders/union/types.ts new file mode 100644 index 0000000..6f82c86 --- /dev/null +++ b/src/core/schemas/builders/union/types.ts @@ -0,0 +1,26 @@ +import { inferParsedObject, inferRawObject, ObjectSchema } from "../object"; +import { Discriminant } from "./discriminant"; + +export type UnionSubtypes = { + [K in DiscriminantValues]: ObjectSchema; +}; + +export type inferRawUnion, U extends UnionSubtypes> = { + [K in keyof U]: Record, K> & inferRawObject; +}[keyof U]; + +export type inferParsedUnion, U extends UnionSubtypes> = { + [K in keyof U]: Record, K> & inferParsedObject; +}[keyof U]; + +export type inferRawDiscriminant> = D extends string + ? D + : D extends Discriminant + ? Raw + : never; + +export type inferParsedDiscriminant> = D extends string + ? D + : D extends Discriminant + ? Parsed + : never; diff --git a/src/core/schemas/builders/union/union.ts b/src/core/schemas/builders/union/union.ts new file mode 100644 index 0000000..ed659be --- /dev/null +++ b/src/core/schemas/builders/union/union.ts @@ -0,0 +1,173 @@ +import { BaseSchema, MaybeValid, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { keys } from "../../utils/keys"; +import { MaybePromise } from "../../utils/MaybePromise"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { enum_ } from "../enum"; +import { ObjectSchema } from "../object"; +import { getObjectLikeUtils, ObjectLikeSchema } from "../object-like"; +import { getSchemaUtils } from "../schema-utils"; +import { Discriminant } from "./discriminant"; +import { inferParsedDiscriminant, inferParsedUnion, inferRawDiscriminant, inferRawUnion, UnionSubtypes } from "./types"; + +export function union, U extends UnionSubtypes>( + discriminant: D, + union: U +): ObjectLikeSchema, inferParsedUnion> { + const rawDiscriminant = + typeof discriminant === "string" ? discriminant : (discriminant.rawDiscriminant as inferRawDiscriminant); + const parsedDiscriminant = + typeof discriminant === "string" + ? discriminant + : (discriminant.parsedDiscriminant as inferParsedDiscriminant); + + const discriminantValueSchema = enum_(keys(union) as string[]); + + const baseSchema: BaseSchema, inferParsedUnion> = { + parse: async (raw, opts) => { + return transformAndValidateUnion({ + value: raw, + discriminant: rawDiscriminant, + transformedDiscriminant: parsedDiscriminant, + transformDiscriminantValue: (discriminantValue) => + discriminantValueSchema.parse(discriminantValue, { + allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawDiscriminant], + }), + getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], + allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, + transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => + additionalPropertiesSchema.parse(additionalProperties, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + json: async (parsed, opts) => { + return transformAndValidateUnion({ + value: parsed, + discriminant: parsedDiscriminant, + transformedDiscriminant: rawDiscriminant, + transformDiscriminantValue: (discriminantValue) => + discriminantValueSchema.json(discriminantValue, { + allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedDiscriminant], + }), + getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], + allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, + transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => + additionalPropertiesSchema.json(additionalProperties, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + getType: () => SchemaType.UNION, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + }; +} + +async function transformAndValidateUnion< + TransformedDiscriminant extends string, + TransformedDiscriminantValue extends string, + TransformedAdditionalProperties +>({ + value, + discriminant, + transformedDiscriminant, + transformDiscriminantValue, + getAdditionalPropertiesSchema, + allowUnrecognizedUnionMembers = false, + transformAdditionalProperties, + breadcrumbsPrefix = [], +}: { + value: unknown; + discriminant: string; + transformedDiscriminant: TransformedDiscriminant; + transformDiscriminantValue: (discriminantValue: unknown) => MaybePromise>; + getAdditionalPropertiesSchema: (discriminantValue: string) => ObjectSchema | undefined; + allowUnrecognizedUnionMembers: boolean | undefined; + transformAdditionalProperties: ( + additionalProperties: unknown, + additionalPropertiesSchema: ObjectSchema + ) => MaybePromise>; + breadcrumbsPrefix: string[] | undefined; +}): Promise< + MaybeValid & TransformedAdditionalProperties> +> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + const { [discriminant]: discriminantValue, ...additionalProperties } = value; + + if (discriminantValue == null) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: `Missing discriminant ("${discriminant}")`, + }, + ], + }; + } + + const transformedDiscriminantValue = await transformDiscriminantValue(discriminantValue); + if (!transformedDiscriminantValue.ok) { + return { + ok: false, + errors: transformedDiscriminantValue.errors, + }; + } + + const additionalPropertiesSchema = getAdditionalPropertiesSchema(transformedDiscriminantValue.value); + + if (additionalPropertiesSchema == null) { + if (allowUnrecognizedUnionMembers) { + return { + ok: true, + value: { + [transformedDiscriminant]: transformedDiscriminantValue.value, + ...additionalProperties, + } as Record & TransformedAdditionalProperties, + }; + } else { + return { + ok: false, + errors: [ + { + path: [...breadcrumbsPrefix, discriminant], + message: "Unexpected discriminant value", + }, + ], + }; + } + } + + const transformedAdditionalProperties = await transformAdditionalProperties( + additionalProperties, + additionalPropertiesSchema + ); + if (!transformedAdditionalProperties.ok) { + return transformedAdditionalProperties; + } + + return { + ok: true, + value: { + [transformedDiscriminant]: discriminantValue, + ...transformedAdditionalProperties.value, + } as Record & TransformedAdditionalProperties, + }; +} diff --git a/src/core/schemas/index.ts b/src/core/schemas/index.ts new file mode 100644 index 0000000..5429d8b --- /dev/null +++ b/src/core/schemas/index.ts @@ -0,0 +1,2 @@ +export * from "./builders"; +export type { inferParsed, inferRaw, Schema, SchemaOptions } from "./Schema"; diff --git a/src/core/schemas/utils/MaybePromise.ts b/src/core/schemas/utils/MaybePromise.ts new file mode 100644 index 0000000..9cd354b --- /dev/null +++ b/src/core/schemas/utils/MaybePromise.ts @@ -0,0 +1 @@ +export type MaybePromise = T | Promise; diff --git a/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts b/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts new file mode 100644 index 0000000..4111d70 --- /dev/null +++ b/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts @@ -0,0 +1,15 @@ +export type addQuestionMarksToNullableProperties = { + [K in OptionalKeys]?: T[K]; +} & Pick>; + +export type OptionalKeys = { + [K in keyof T]-?: undefined extends T[K] + ? K + : null extends T[K] + ? K + : 1 extends (any extends T[K] ? 0 : 1) + ? never + : K; +}[keyof T]; + +export type RequiredKeys = Exclude>; diff --git a/src/core/schemas/utils/createIdentitySchemaCreator.ts b/src/core/schemas/utils/createIdentitySchemaCreator.ts new file mode 100644 index 0000000..de107cf --- /dev/null +++ b/src/core/schemas/utils/createIdentitySchemaCreator.ts @@ -0,0 +1,21 @@ +import { getSchemaUtils } from "../builders/schema-utils"; +import { BaseSchema, MaybeValid, Schema, SchemaOptions, SchemaType } from "../Schema"; +import { maybeSkipValidation } from "./maybeSkipValidation"; + +export function createIdentitySchemaCreator( + schemaType: SchemaType, + validate: (value: unknown, opts?: SchemaOptions) => MaybeValid +): () => Schema { + return () => { + const baseSchema: BaseSchema = { + parse: validate, + json: validate, + getType: () => schemaType, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; + }; +} diff --git a/src/core/schemas/utils/entries.ts b/src/core/schemas/utils/entries.ts new file mode 100644 index 0000000..e122952 --- /dev/null +++ b/src/core/schemas/utils/entries.ts @@ -0,0 +1,3 @@ +export function entries(object: T): [keyof T, T[keyof T]][] { + return Object.entries(object) as [keyof T, T[keyof T]][]; +} diff --git a/src/core/schemas/utils/filterObject.ts b/src/core/schemas/utils/filterObject.ts new file mode 100644 index 0000000..2c25a34 --- /dev/null +++ b/src/core/schemas/utils/filterObject.ts @@ -0,0 +1,10 @@ +export function filterObject(obj: T, keysToInclude: K[]): Pick { + const keysToIncludeSet = new Set(keysToInclude); + return Object.entries(obj).reduce((acc, [key, value]) => { + if (keysToIncludeSet.has(key as K)) { + acc[key as K] = value; + } + return acc; + // eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter + }, {} as Pick); +} diff --git a/src/core/schemas/utils/getErrorMessageForIncorrectType.ts b/src/core/schemas/utils/getErrorMessageForIncorrectType.ts new file mode 100644 index 0000000..438012d --- /dev/null +++ b/src/core/schemas/utils/getErrorMessageForIncorrectType.ts @@ -0,0 +1,21 @@ +export function getErrorMessageForIncorrectType(value: unknown, expectedType: string): string { + return `Expected ${expectedType}. Received ${getTypeAsString(value)}.`; +} + +function getTypeAsString(value: unknown): string { + if (Array.isArray(value)) { + return "list"; + } + if (value === null) { + return "null"; + } + switch (typeof value) { + case "string": + return `"${value}"`; + case "number": + case "boolean": + case "undefined": + return `${value}`; + } + return typeof value; +} diff --git a/src/core/schemas/utils/isPlainObject.ts b/src/core/schemas/utils/isPlainObject.ts new file mode 100644 index 0000000..db82a72 --- /dev/null +++ b/src/core/schemas/utils/isPlainObject.ts @@ -0,0 +1,17 @@ +// borrowed from https://github.com/lodash/lodash/blob/master/isPlainObject.js +export function isPlainObject(value: unknown): value is Record { + if (typeof value !== "object" || value === null) { + return false; + } + + if (Object.getPrototypeOf(value) === null) { + return true; + } + + let proto = value; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + + return Object.getPrototypeOf(value) === proto; +} diff --git a/src/core/schemas/utils/keys.ts b/src/core/schemas/utils/keys.ts new file mode 100644 index 0000000..0186709 --- /dev/null +++ b/src/core/schemas/utils/keys.ts @@ -0,0 +1,3 @@ +export function keys(object: T): (keyof T)[] { + return Object.keys(object) as (keyof T)[]; +} diff --git a/src/core/schemas/utils/maybeSkipValidation.ts b/src/core/schemas/utils/maybeSkipValidation.ts new file mode 100644 index 0000000..99c02c3 --- /dev/null +++ b/src/core/schemas/utils/maybeSkipValidation.ts @@ -0,0 +1,39 @@ +import { BaseSchema, MaybeValid, SchemaOptions } from "../Schema"; +import { MaybePromise } from "./MaybePromise"; + +export function maybeSkipValidation, Raw, Parsed>(schema: S): S { + return { + ...schema, + json: transformAndMaybeSkipValidation(schema.json), + parse: transformAndMaybeSkipValidation(schema.parse), + }; +} + +function transformAndMaybeSkipValidation( + transform: (value: unknown, opts?: SchemaOptions) => MaybePromise> +): (value: unknown, opts?: SchemaOptions) => MaybePromise> { + return async (value, opts): Promise> => { + const transformed = await transform(value, opts); + const { skipValidation = false } = opts ?? {}; + if (!transformed.ok && skipValidation) { + // eslint-disable-next-line no-console + console.warn( + [ + "Failed to validate.", + ...transformed.errors.map( + (error) => + " - " + + (error.path.length > 0 ? `${error.path.join(".")}: ${error.message}` : error.message) + ), + ].join("\n") + ); + + return { + ok: true, + value: value as T, + }; + } else { + return transformed; + } + }; +} diff --git a/src/core/schemas/utils/partition.ts b/src/core/schemas/utils/partition.ts new file mode 100644 index 0000000..f58d6f3 --- /dev/null +++ b/src/core/schemas/utils/partition.ts @@ -0,0 +1,12 @@ +export function partition(items: readonly T[], predicate: (item: T) => boolean): [T[], T[]] { + const trueItems: T[] = [], + falseItems: T[] = []; + for (const item of items) { + if (predicate(item)) { + trueItems.push(item); + } else { + falseItems.push(item); + } + } + return [trueItems, falseItems]; +} diff --git a/src/environments.ts b/src/environments.ts new file mode 100644 index 0000000..81ada4a --- /dev/null +++ b/src/environments.ts @@ -0,0 +1,9 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export const SyndicateEnvironment = { + Production: "https://api.syndicate.io", +} as const; + +export type SyndicateEnvironment = typeof SyndicateEnvironment.Production; diff --git a/src/errors/SyndicateError.ts b/src/errors/SyndicateError.ts new file mode 100644 index 0000000..e8fc224 --- /dev/null +++ b/src/errors/SyndicateError.ts @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export class SyndicateError extends Error { + readonly statusCode?: number; + readonly body?: unknown; + + constructor({ message, statusCode, body }: { message?: string; statusCode?: number; body?: unknown }) { + super(buildMessage({ message, statusCode, body })); + Object.setPrototypeOf(this, SyndicateError.prototype); + if (statusCode != null) { + this.statusCode = statusCode; + } + + if (body !== undefined) { + this.body = body; + } + } +} + +function buildMessage({ + message, + statusCode, + body, +}: { + message: string | undefined; + statusCode: number | undefined; + body: unknown | undefined; +}): string { + let lines: string[] = []; + if (message != null) { + lines.push(message); + } + + if (statusCode != null) { + lines.push(`Status code: ${statusCode.toString()}`); + } + + if (body != null) { + lines.push(`Body: ${JSON.stringify(body, undefined, 2)}`); + } + + return lines.join("\n"); +} diff --git a/src/errors/SyndicateTimeoutError.ts b/src/errors/SyndicateTimeoutError.ts new file mode 100644 index 0000000..730ce09 --- /dev/null +++ b/src/errors/SyndicateTimeoutError.ts @@ -0,0 +1,10 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export class SyndicateTimeoutError extends Error { + constructor() { + super("Timeout"); + Object.setPrototypeOf(this, SyndicateTimeoutError.prototype); + } +} diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..ab2814f --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,2 @@ +export { SyndicateError } from "./SyndicateError"; +export { SyndicateTimeoutError } from "./SyndicateTimeoutError"; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..348f873 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +export * as Syndicate from "./api"; +export { SyndicateClient } from "./Client"; +export { SyndicateEnvironment } from "./environments"; +export { SyndicateError, SyndicateTimeoutError } from "./errors"; diff --git a/src/serialization/index.ts b/src/serialization/index.ts new file mode 100644 index 0000000..3e5335f --- /dev/null +++ b/src/serialization/index.ts @@ -0,0 +1 @@ +export * from "./resources"; diff --git a/src/serialization/resources/index.ts b/src/serialization/resources/index.ts new file mode 100644 index 0000000..e3d6383 --- /dev/null +++ b/src/serialization/resources/index.ts @@ -0,0 +1,2 @@ +export * as transact from "./transact"; +export * as wallet from "./wallet"; diff --git a/src/serialization/resources/transact/index.ts b/src/serialization/resources/transact/index.ts new file mode 100644 index 0000000..eea524d --- /dev/null +++ b/src/serialization/resources/transact/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/src/serialization/resources/transact/types/ErrorWithMessage.ts b/src/serialization/resources/transact/types/ErrorWithMessage.ts new file mode 100644 index 0000000..bdd4b28 --- /dev/null +++ b/src/serialization/resources/transact/types/ErrorWithMessage.ts @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const ErrorWithMessage: core.serialization.ObjectSchema< + serializers.transact.ErrorWithMessage.Raw, + Syndicate.transact.ErrorWithMessage +> = core.serialization.object({ + message: core.serialization.string(), +}); + +export declare namespace ErrorWithMessage { + interface Raw { + message: string; + } +} diff --git a/src/serialization/resources/transact/types/SendTransactionRequest.ts b/src/serialization/resources/transact/types/SendTransactionRequest.ts new file mode 100644 index 0000000..80e7dd4 --- /dev/null +++ b/src/serialization/resources/transact/types/SendTransactionRequest.ts @@ -0,0 +1,30 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const SendTransactionRequest: core.serialization.ObjectSchema< + serializers.transact.SendTransactionRequest.Raw, + Syndicate.transact.SendTransactionRequest +> = core.serialization.object({ + requestId: core.serialization.string().optional(), + projectId: core.serialization.string(), + contractAddress: core.serialization.string(), + chainId: core.serialization.number(), + functionSignature: core.serialization.string(), + args: core.serialization.record(core.serialization.string(), core.serialization.unknown()).optional(), +}); + +export declare namespace SendTransactionRequest { + interface Raw { + requestId?: string | null; + projectId: string; + contractAddress: string; + chainId: number; + functionSignature: string; + args?: Record | null; + } +} diff --git a/src/serialization/resources/transact/types/SendTransactionResponse.ts b/src/serialization/resources/transact/types/SendTransactionResponse.ts new file mode 100644 index 0000000..c18ee9b --- /dev/null +++ b/src/serialization/resources/transact/types/SendTransactionResponse.ts @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const SendTransactionResponse: core.serialization.ObjectSchema< + serializers.transact.SendTransactionResponse.Raw, + Syndicate.transact.SendTransactionResponse +> = core.serialization.object({ + transactionId: core.serialization.string(), +}); + +export declare namespace SendTransactionResponse { + interface Raw { + transactionId: string; + } +} diff --git a/src/serialization/resources/transact/types/index.ts b/src/serialization/resources/transact/types/index.ts new file mode 100644 index 0000000..92e394f --- /dev/null +++ b/src/serialization/resources/transact/types/index.ts @@ -0,0 +1,3 @@ +export * from "./ErrorWithMessage"; +export * from "./SendTransactionRequest"; +export * from "./SendTransactionResponse"; diff --git a/src/serialization/resources/wallet/client/getWalletsByProject.ts b/src/serialization/resources/wallet/client/getWalletsByProject.ts new file mode 100644 index 0000000..ee1d25f --- /dev/null +++ b/src/serialization/resources/wallet/client/getWalletsByProject.ts @@ -0,0 +1,18 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const Response: core.serialization.Schema< + serializers.wallet.getWalletsByProject.Response.Raw, + Syndicate.wallet.WalletWithTxCountAndBalance[] +> = core.serialization.list( + core.serialization.lazyObject(async () => (await import("../../..")).wallet.WalletWithTxCountAndBalance) +); + +export declare namespace Response { + type Raw = serializers.wallet.WalletWithTxCountAndBalance.Raw[]; +} diff --git a/src/serialization/resources/wallet/client/index.ts b/src/serialization/resources/wallet/client/index.ts new file mode 100644 index 0000000..9e77e94 --- /dev/null +++ b/src/serialization/resources/wallet/client/index.ts @@ -0,0 +1 @@ +export * as getWalletsByProject from "./getWalletsByProject"; diff --git a/src/serialization/resources/wallet/index.ts b/src/serialization/resources/wallet/index.ts new file mode 100644 index 0000000..c9240f8 --- /dev/null +++ b/src/serialization/resources/wallet/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./client"; diff --git a/src/serialization/resources/wallet/types/CreateWalletErrorBody.ts b/src/serialization/resources/wallet/types/CreateWalletErrorBody.ts new file mode 100644 index 0000000..5d7f89b --- /dev/null +++ b/src/serialization/resources/wallet/types/CreateWalletErrorBody.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const CreateWalletErrorBody: core.serialization.ObjectSchema< + serializers.wallet.CreateWalletErrorBody.Raw, + Syndicate.wallet.CreateWalletErrorBody +> = core.serialization.object({ + projectId: core.serialization.string(), + message: core.serialization.string(), +}); + +export declare namespace CreateWalletErrorBody { + interface Raw { + projectId: string; + message: string; + } +} diff --git a/src/serialization/resources/wallet/types/CreateWalletRequest.ts b/src/serialization/resources/wallet/types/CreateWalletRequest.ts new file mode 100644 index 0000000..3b7704a --- /dev/null +++ b/src/serialization/resources/wallet/types/CreateWalletRequest.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const CreateWalletRequest: core.serialization.ObjectSchema< + serializers.wallet.CreateWalletRequest.Raw, + Syndicate.wallet.CreateWalletRequest +> = core.serialization.object({ + projectId: core.serialization.string(), + chainId: core.serialization.number(), +}); + +export declare namespace CreateWalletRequest { + interface Raw { + projectId: string; + chainId: number; + } +} diff --git a/src/serialization/resources/wallet/types/CreateWalletResponse.ts b/src/serialization/resources/wallet/types/CreateWalletResponse.ts new file mode 100644 index 0000000..4316e14 --- /dev/null +++ b/src/serialization/resources/wallet/types/CreateWalletResponse.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const CreateWalletResponse: core.serialization.ObjectSchema< + serializers.wallet.CreateWalletResponse.Raw, + Syndicate.wallet.CreateWalletResponse +> = core.serialization.object({ + projectId: core.serialization.string(), + walletAddress: core.serialization.string(), +}); + +export declare namespace CreateWalletResponse { + interface Raw { + projectId: string; + walletAddress: string; + } +} diff --git a/src/serialization/resources/wallet/types/ProjectTransactionStatsResponse.ts b/src/serialization/resources/wallet/types/ProjectTransactionStatsResponse.ts new file mode 100644 index 0000000..8a31b30 --- /dev/null +++ b/src/serialization/resources/wallet/types/ProjectTransactionStatsResponse.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const ProjectTransactionStatsResponse: core.serialization.ObjectSchema< + serializers.wallet.ProjectTransactionStatsResponse.Raw, + Syndicate.wallet.ProjectTransactionStatsResponse +> = core.serialization.object({ + numberOfTransactions: core.serialization.number(), + numberOfFailedTransactions: core.serialization.number(), +}); + +export declare namespace ProjectTransactionStatsResponse { + interface Raw { + numberOfTransactions: number; + numberOfFailedTransactions: number; + } +} diff --git a/src/serialization/resources/wallet/types/RetireWalletRequest.ts b/src/serialization/resources/wallet/types/RetireWalletRequest.ts new file mode 100644 index 0000000..def58c0 --- /dev/null +++ b/src/serialization/resources/wallet/types/RetireWalletRequest.ts @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const RetireWalletRequest: core.serialization.ObjectSchema< + serializers.wallet.RetireWalletRequest.Raw, + Syndicate.wallet.RetireWalletRequest +> = core.serialization.object({ + projectId: core.serialization.string(), + walletAddress: core.serialization.string(), + gasRecipient: core.serialization.string().optional(), +}); + +export declare namespace RetireWalletRequest { + interface Raw { + projectId: string; + walletAddress: string; + gasRecipient?: string | null; + } +} diff --git a/src/serialization/resources/wallet/types/RetireWalletResponse.ts b/src/serialization/resources/wallet/types/RetireWalletResponse.ts new file mode 100644 index 0000000..9775751 --- /dev/null +++ b/src/serialization/resources/wallet/types/RetireWalletResponse.ts @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const RetireWalletResponse: core.serialization.ObjectSchema< + serializers.wallet.RetireWalletResponse.Raw, + Syndicate.wallet.RetireWalletResponse +> = core.serialization.object({ + projectId: core.serialization.string(), + walletAddress: core.serialization.string(), + transactionId: core.serialization.string().optional(), +}); + +export declare namespace RetireWalletResponse { + interface Raw { + projectId: string; + walletAddress: string; + transactionId?: string | null; + } +} diff --git a/src/serialization/resources/wallet/types/TransactionAttempt.ts b/src/serialization/resources/wallet/types/TransactionAttempt.ts new file mode 100644 index 0000000..52da840 --- /dev/null +++ b/src/serialization/resources/wallet/types/TransactionAttempt.ts @@ -0,0 +1,40 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const TransactionAttempt: core.serialization.ObjectSchema< + serializers.wallet.TransactionAttempt.Raw, + Syndicate.wallet.TransactionAttempt +> = core.serialization.object({ + transactionId: core.serialization.string(), + hash: core.serialization.string(), + chainId: core.serialization.number(), + status: core.serialization.lazy(async () => (await import("../../..")).wallet.TransactionStatus), + block: core.serialization.number(), + signedTxn: core.serialization.string(), + walletAddress: core.serialization.string(), + reverted: core.serialization.boolean(), + nonce: core.serialization.number(), + createdAt: core.serialization.date(), + updatedAt: core.serialization.date(), +}); + +export declare namespace TransactionAttempt { + interface Raw { + transactionId: string; + hash: string; + chainId: number; + status: serializers.wallet.TransactionStatus.Raw; + block: number; + signedTxn: string; + walletAddress: string; + reverted: boolean; + nonce: number; + createdAt: string; + updatedAt: string; + } +} diff --git a/src/serialization/resources/wallet/types/TransactionRequestsByProjectResponse.ts b/src/serialization/resources/wallet/types/TransactionRequestsByProjectResponse.ts new file mode 100644 index 0000000..94a340c --- /dev/null +++ b/src/serialization/resources/wallet/types/TransactionRequestsByProjectResponse.ts @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const TransactionRequestsByProjectResponse: core.serialization.ObjectSchema< + serializers.wallet.TransactionRequestsByProjectResponse.Raw, + Syndicate.wallet.TransactionRequestsByProjectResponse +> = core.serialization.object({ + transactionRequests: core.serialization.list( + core.serialization.lazyObject(async () => (await import("../../..")).wallet.TransactionResponse) + ), + total: core.serialization.number(), +}); + +export declare namespace TransactionRequestsByProjectResponse { + interface Raw { + transactionRequests: serializers.wallet.TransactionResponse.Raw[]; + total: number; + } +} diff --git a/src/serialization/resources/wallet/types/TransactionResponse.ts b/src/serialization/resources/wallet/types/TransactionResponse.ts new file mode 100644 index 0000000..c792897 --- /dev/null +++ b/src/serialization/resources/wallet/types/TransactionResponse.ts @@ -0,0 +1,42 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const TransactionResponse: core.serialization.ObjectSchema< + serializers.wallet.TransactionResponse.Raw, + Syndicate.wallet.TransactionResponse +> = core.serialization.object({ + transactionId: core.serialization.string(), + chainId: core.serialization.number(), + projectId: core.serialization.string(), + invalid: core.serialization.boolean(), + contractAddress: core.serialization.string(), + functionSignature: core.serialization.string(), + data: core.serialization.string(), + value: core.serialization.string(), + createdAt: core.serialization.date(), + updatedAt: core.serialization.date(), + transactionAttempts: core.serialization + .list(core.serialization.lazyObject(async () => (await import("../../..")).wallet.TransactionAttempt)) + .optional(), +}); + +export declare namespace TransactionResponse { + interface Raw { + transactionId: string; + chainId: number; + projectId: string; + invalid: boolean; + contractAddress: string; + functionSignature: string; + data: string; + value: string; + createdAt: string; + updatedAt: string; + transactionAttempts?: serializers.wallet.TransactionAttempt.Raw[] | null; + } +} diff --git a/src/serialization/resources/wallet/types/TransactionStatus.ts b/src/serialization/resources/wallet/types/TransactionStatus.ts new file mode 100644 index 0000000..4ad6226 --- /dev/null +++ b/src/serialization/resources/wallet/types/TransactionStatus.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const TransactionStatus: core.serialization.Schema< + serializers.wallet.TransactionStatus.Raw, + Syndicate.wallet.TransactionStatus +> = core.serialization.enum_(["PENDING", "SUBMITTED", "CONFIRMED"]); + +export declare namespace TransactionStatus { + type Raw = "PENDING" | "SUBMITTED" | "CONFIRMED"; +} diff --git a/src/serialization/resources/wallet/types/TransactionsByProjectResponse.ts b/src/serialization/resources/wallet/types/TransactionsByProjectResponse.ts new file mode 100644 index 0000000..63f9910 --- /dev/null +++ b/src/serialization/resources/wallet/types/TransactionsByProjectResponse.ts @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const TransactionsByProjectResponse: core.serialization.ObjectSchema< + serializers.wallet.TransactionsByProjectResponse.Raw, + Syndicate.wallet.TransactionsByProjectResponse +> = core.serialization.object({ + transactionAttempts: core.serialization.list( + core.serialization.lazyObject(async () => (await import("../../..")).wallet.TransactionAttempt) + ), + total: core.serialization.number(), +}); + +export declare namespace TransactionsByProjectResponse { + interface Raw { + transactionAttempts: serializers.wallet.TransactionAttempt.Raw[]; + total: number; + } +} diff --git a/src/serialization/resources/wallet/types/Wallet.ts b/src/serialization/resources/wallet/types/Wallet.ts new file mode 100644 index 0000000..85022cc --- /dev/null +++ b/src/serialization/resources/wallet/types/Wallet.ts @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const Wallet: core.serialization.ObjectSchema = + core.serialization.object({ + walletId: core.serialization.string(), + walletAddress: core.serialization.string(), + chainId: core.serialization.number(), + nonce: core.serialization.number(), + isActive: core.serialization.boolean(), + projectId: core.serialization.string(), + createdAt: core.serialization.date(), + updatedAt: core.serialization.date(), + }); + +export declare namespace Wallet { + interface Raw { + walletId: string; + walletAddress: string; + chainId: number; + nonce: number; + isActive: boolean; + projectId: string; + createdAt: string; + updatedAt: string; + } +} diff --git a/src/serialization/resources/wallet/types/WalletWithTxCountAndBalance.ts b/src/serialization/resources/wallet/types/WalletWithTxCountAndBalance.ts new file mode 100644 index 0000000..209e1ef --- /dev/null +++ b/src/serialization/resources/wallet/types/WalletWithTxCountAndBalance.ts @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../.."; +import * as Syndicate from "../../../../api"; +import * as core from "../../../../core"; + +export const WalletWithTxCountAndBalance: core.serialization.ObjectSchema< + serializers.wallet.WalletWithTxCountAndBalance.Raw, + Syndicate.wallet.WalletWithTxCountAndBalance +> = core.serialization + .object({ + txCount: core.serialization.number().optional(), + balance: core.serialization.string().optional(), + }) + .extend(core.serialization.lazyObject(async () => (await import("../../..")).wallet.Wallet)); + +export declare namespace WalletWithTxCountAndBalance { + interface Raw extends serializers.wallet.Wallet.Raw { + txCount?: number | null; + balance?: string | null; + } +} diff --git a/src/serialization/resources/wallet/types/index.ts b/src/serialization/resources/wallet/types/index.ts new file mode 100644 index 0000000..bc5c57e --- /dev/null +++ b/src/serialization/resources/wallet/types/index.ts @@ -0,0 +1,13 @@ +export * from "./Wallet"; +export * from "./CreateWalletRequest"; +export * from "./CreateWalletResponse"; +export * from "./CreateWalletErrorBody"; +export * from "./WalletWithTxCountAndBalance"; +export * from "./RetireWalletRequest"; +export * from "./RetireWalletResponse"; +export * from "./TransactionResponse"; +export * from "./TransactionAttempt"; +export * from "./TransactionsByProjectResponse"; +export * from "./TransactionRequestsByProjectResponse"; +export * from "./TransactionStatus"; +export * from "./ProjectTransactionStatsResponse"; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e65fa53 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "extendedDiagnostics": true, + "strict": true, + "target": "ES6", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "noUnusedParameters": true, + "outDir": "dist", + "rootDir": "src", + "baseUrl": "src" + }, + "include": [ + "src" + ], + "exclude": [] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..e3d7ca2 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,89 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@17.0.33": + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.33.tgz#3c1879b276dc63e73030bb91165e62a4509cd506" + integrity sha512-miWq2m2FiQZmaHfdZNcbpp9PuXg34W5JZ5CrJ/BaS70VuhoJENBEQybeiYSaPBRNq6KQGnjfEnc/F3PN++D+XQ== + +"@types/url-join@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-4.0.1.tgz#4989c97f969464647a8586c7252d97b449cdc045" + integrity sha512-wDXw9LEEUHyV+7UWy7U315nrJGJ7p1BzaCxDpEoLr789Dk1WDVMMlf3iBfbG2F8NdWnYyFbtTxUn2ZNbm1Q4LQ== + +"@ungap/url-search-params@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@ungap/url-search-params/-/url-search-params-0.2.2.tgz#2de3bdec21476a9b70ef11fd7b794752f9afa04c" + integrity sha512-qQsguKXZVKdCixOHX9jqnX/K/1HekPDpGKyEcXHT+zR6EjGA7S4boSuelL4uuPv6YfhN0n8c4UxW+v/Z3gM2iw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +follow-redirects@^1.14.9: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +js-base64@3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745" + integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +prettier@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + +typescript@4.6.4: + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== + +url-join@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==