diff --git a/packages/core/aqua-to-js/package.json b/packages/core/aqua-to-js/package.json index 8a57092d..3c7b6a7f 100644 --- a/packages/core/aqua-to-js/package.json +++ b/packages/core/aqua-to-js/package.json @@ -22,7 +22,8 @@ "@fluencelabs/spell": "0.5.20", "@fluencelabs/trust-graph": "0.4.7", "ts-pattern": "5.0.5", - "vitest": "0.34.6" + "vitest": "0.34.6", + "zod": "3.22.4" }, "peerDependencies": { "@fluencelabs/js-client": "workspace:^" diff --git a/packages/core/aqua-to-js/src/index.ts b/packages/core/aqua-to-js/src/index.ts index 44c21160..27de5240 100644 --- a/packages/core/aqua-to-js/src/index.ts +++ b/packages/core/aqua-to-js/src/index.ts @@ -27,17 +27,14 @@ interface TsOutput { sources: string; } -type LanguageOutput = { - js: JsOutput; - ts: TsOutput; -}; +type LanguageOutput = JsOutput | TsOutput; type NothingToGenerate = null; -export default async function aquaToJs( +export default async function aquaToJs( res: CompilationResult, - outputType: T, -): Promise { + outputType: OutputType, +): Promise { if ( Object.keys(res.services).length === 0 && Object.keys(res.functions).length === 0 @@ -49,12 +46,10 @@ export default async function aquaToJs( return outputType === "js" ? { - sources: generateSources(res, "js", packageJson), + sources: generateSources(res, outputType, packageJson), types: generateTypes(res, packageJson), } - : // TODO: probably there is a way to remove this type assert - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ({ - sources: generateSources(res, "ts", packageJson), - } as LanguageOutput[T]); + : { + sources: generateSources(res, outputType, packageJson), + }; } diff --git a/packages/core/aqua-to-js/src/utils.ts b/packages/core/aqua-to-js/src/utils.ts index 489b6696..32e5d209 100644 --- a/packages/core/aqua-to-js/src/utils.ts +++ b/packages/core/aqua-to-js/src/utils.ts @@ -28,14 +28,17 @@ import { SimpleTypes, UnlabeledProductType, } from "@fluencelabs/interfaces"; +import { z } from "zod"; -export interface PackageJson { - name: string; - version: string; - devDependencies: { - ["@fluencelabs/aqua-api"]: string; - }; -} +const packageJsonSchema = z.object({ + name: z.string(), + version: z.string(), + devDependencies: z.object({ + ["@fluencelabs/aqua-api"]: z.string(), + }), +}); + +export type PackageJson = z.infer; export async function getPackageJsonContent(): Promise { const content = await readFile( @@ -43,9 +46,7 @@ export async function getPackageJsonContent(): Promise { "utf-8", ); - // TODO: Add validation here - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return JSON.parse(content) as PackageJson; + return packageJsonSchema.parse(JSON.parse(content)); } export function getFuncArgs( diff --git a/packages/core/js-client-isomorphic/package.json b/packages/core/js-client-isomorphic/package.json index def6c79c..68c913f1 100644 --- a/packages/core/js-client-isomorphic/package.json +++ b/packages/core/js-client-isomorphic/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@fluencelabs/avm": "0.54.0", - "@fluencelabs/marine-js": "0.7.2", + "@fluencelabs/marine-js": "0.8.0", "@fluencelabs/marine-worker": "0.4.2", "@fluencelabs/threads": "^2.0.0" }, diff --git a/packages/core/js-client-isomorphic/src/fetchers/browser.ts b/packages/core/js-client-isomorphic/src/fetchers/browser.ts index dc4f547b..939c5936 100644 --- a/packages/core/js-client-isomorphic/src/fetchers/browser.ts +++ b/packages/core/js-client-isomorphic/src/fetchers/browser.ts @@ -14,18 +14,14 @@ * limitations under the License. */ -import { FetchedPackages, getVersionedPackage } from "../types.js"; +import { FetchResourceFn, getVersionedPackage } from "../types.js"; /** * @param pkg name of package with version * @param assetPath path of required asset in given package * @param root CDN domain in browser or file system root in node */ -export async function fetchResource( - pkg: FetchedPackages, - assetPath: string, - root: string, -) { +export const fetchResource: FetchResourceFn = async (pkg, assetPath, root) => { const refinedAssetPath = assetPath.startsWith("/") ? assetPath.slice(1) : assetPath; @@ -36,4 +32,4 @@ export async function fetchResource( return fetch(url).catch(() => { throw new Error(`Cannot fetch from ${url.toString()}`); }); -} +}; diff --git a/packages/core/js-client-isomorphic/src/fetchers/node.ts b/packages/core/js-client-isomorphic/src/fetchers/node.ts index 114639b1..8b9ab6b9 100644 --- a/packages/core/js-client-isomorphic/src/fetchers/node.ts +++ b/packages/core/js-client-isomorphic/src/fetchers/node.ts @@ -18,21 +18,14 @@ import { readFile } from "fs/promises"; import { createRequire } from "module"; import { sep, posix, join } from "path"; -import { FetchedPackages, getVersionedPackage } from "../types.js"; +import { FetchResourceFn, getVersionedPackage } from "../types.js"; /** * @param pkg name of package with version * @param assetPath path of required asset in given package - * @param root CDN domain in browser or js-client itself in node */ -export async function fetchResource( - pkg: FetchedPackages, - assetPath: string, - root: string, -) { +export const fetchResource: FetchResourceFn = async (pkg, assetPath) => { const { name } = getVersionedPackage(pkg); - // TODO: `root` will be handled somehow in the future. For now, we use filesystem root where js-client is running; - root = "/"; const require = createRequire(import.meta.url); const packagePathIndex = require.resolve(name); @@ -47,7 +40,7 @@ export async function fetchResource( throw new Error(`Cannot find dependency ${name} in path ${posixPath}`); } - const pathToResource = join(root, packagePath, assetPath); + const pathToResource = join(packagePath, assetPath); const file = await readFile(pathToResource); @@ -60,4 +53,4 @@ export async function fetchResource( : "application/text", }, }); -} +}; diff --git a/packages/core/js-client-isomorphic/src/types.ts b/packages/core/js-client-isomorphic/src/types.ts index c9dab2e4..6e81bf27 100644 --- a/packages/core/js-client-isomorphic/src/types.ts +++ b/packages/core/js-client-isomorphic/src/types.ts @@ -20,7 +20,7 @@ import versions from "./versions.js"; export type FetchedPackages = keyof typeof versions; type VersionedPackage = { name: string; version: string }; -export type GetWorker = ( +export type GetWorkerFn = ( pkg: FetchedPackages, CDNUrl: string, ) => Promise; @@ -31,3 +31,9 @@ export const getVersionedPackage = (pkg: FetchedPackages): VersionedPackage => { version: versions[pkg], }; }; + +export type FetchResourceFn = ( + pkg: FetchedPackages, + assetPath: string, + root: string, +) => Promise; diff --git a/packages/core/js-client-isomorphic/src/worker-resolvers/browser.ts b/packages/core/js-client-isomorphic/src/worker-resolvers/browser.ts index 5d37014f..7e2b1e71 100644 --- a/packages/core/js-client-isomorphic/src/worker-resolvers/browser.ts +++ b/packages/core/js-client-isomorphic/src/worker-resolvers/browser.ts @@ -17,9 +17,9 @@ import { BlobWorker } from "@fluencelabs/threads/master"; import { fetchResource } from "../fetchers/browser.js"; -import type { FetchedPackages, GetWorker } from "../types.js"; +import type { FetchedPackages, GetWorkerFn } from "../types.js"; -export const getWorker: GetWorker = async ( +export const getWorker: GetWorkerFn = async ( pkg: FetchedPackages, CDNUrl: string, ) => { diff --git a/packages/core/js-client-isomorphic/src/worker-resolvers/node.ts b/packages/core/js-client-isomorphic/src/worker-resolvers/node.ts index 85b9aa6c..44c49d70 100644 --- a/packages/core/js-client-isomorphic/src/worker-resolvers/node.ts +++ b/packages/core/js-client-isomorphic/src/worker-resolvers/node.ts @@ -20,10 +20,10 @@ import { fileURLToPath } from "url"; import { Worker } from "@fluencelabs/threads/master"; -import type { FetchedPackages, GetWorker } from "../types.js"; +import type { FetchedPackages, GetWorkerFn } from "../types.js"; import { getVersionedPackage } from "../types.js"; -export const getWorker: GetWorker = (pkg: FetchedPackages) => { +export const getWorker: GetWorkerFn = (pkg: FetchedPackages) => { const require = createRequire(import.meta.url); const pathToThisFile = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/core/js-client/package.json b/packages/core/js-client/package.json index 8db416cc..66cb9c46 100644 --- a/packages/core/js-client/package.json +++ b/packages/core/js-client/package.json @@ -43,10 +43,10 @@ "@libp2p/peer-id-factory": "3.0.3", "@libp2p/websockets": "7.0.4", "@multiformats/multiaddr": "11.3.0", - "assert": "2.1.0", "async": "3.2.4", "bs58": "5.0.0", "buffer": "6.0.3", + "class-transformer": "0.5.1", "debug": "4.3.4", "it-length-prefixed": "8.0.4", "it-map": "2.0.0", @@ -57,11 +57,12 @@ "rxjs": "7.5.5", "uint8arrays": "4.0.3", "uuid": "8.3.2", - "zod": "3.22.4" + "zod": "3.22.4", + "zod-validation-error": "2.1.0" }, "devDependencies": { "@fluencelabs/aqua-api": "0.9.3", - "@fluencelabs/marine-js": "0.7.2", + "@fluencelabs/marine-js": "0.8.0", "@rollup/plugin-inject": "5.0.3", "@types/bs58": "4.0.1", "@types/debug": "4.1.7", diff --git a/packages/core/js-client/src/clientPeer/types.ts b/packages/core/js-client/src/clientPeer/types.ts index 8afc7565..9dbe2a86 100644 --- a/packages/core/js-client/src/clientPeer/types.ts +++ b/packages/core/js-client/src/clientPeer/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { z } from "zod"; + /** * Peer ID's id as a base58 string (multihash/CIDv0). */ @@ -33,20 +35,30 @@ export type Node = { * - string: multiaddr in string format * - Node: node structure, @see Node */ -export type RelayOptions = string | Node; +export const relaySchema = z.union([ + z.string(), + z.object({ + peerId: z.string(), + multiaddr: z.string(), + }), +]); + +export type RelayOptions = z.infer; /** * Fluence Peer's key pair types */ export type KeyTypes = "RSA" | "Ed25519" | "secp256k1"; +const keyPairOptionsSchema = z.object({ + type: z.literal("Ed25519"), + source: z.union([z.literal("random"), z.instanceof(Uint8Array)]), +}); + /** * Options to specify key pair used in Fluence Peer */ -export type KeyPairOptions = { - type: "Ed25519"; - source: "random" | Uint8Array; -}; +export type KeyPairOptions = z.infer; /** * Fluence JS Client connection states as string literals @@ -63,17 +75,10 @@ export const ConnectionStates = [ */ export type ConnectionState = (typeof ConnectionStates)[number]; -export interface IFluenceInternalApi { - /** - * Internal API - */ - internals: unknown; -} - /** * Public API of Fluence JS Client */ -export interface IFluenceClient extends IFluenceInternalApi { +export interface IFluenceClient { /** * Connect to the Fluence network */ @@ -107,65 +112,66 @@ export interface IFluenceClient extends IFluenceInternalApi { getRelayPeerId(): string; } +export const configSchema = z + .object({ + /** + * Specify the KeyPair to be used to identify the Fluence Peer. + * Will be generated randomly if not specified + */ + keyPair: keyPairOptionsSchema, + /** + * Options to configure the connection to the Fluence network + */ + connectionOptions: z + .object({ + /** + * When the peer established the connection to the network it sends a ping-like message to check if it works correctly. + * The options allows to specify the timeout for that message in milliseconds. + * If not specified the default timeout will be used + */ + skipCheckConnection: z.boolean(), + /** + * The dialing timeout in milliseconds + */ + dialTimeoutMs: z.number(), + /** + * The maximum number of inbound streams for the libp2p node. + * Default: 1024 + */ + maxInboundStreams: z.number(), + /** + * The maximum number of outbound streams for the libp2p node. + * Default: 1024 + */ + maxOutboundStreams: z.number(), + }) + .partial(), + /** + * Sets the default TTL for all particles originating from the peer with no TTL specified. + * If the originating particle's TTL is defined then that value will be used + * If the option is not set default TTL will be 7000 + */ + defaultTtlMs: z.number(), + /** + * Property for passing custom CDN Url to load dependencies from browser. https://unpkg.com used by default + */ + CDNUrl: z.string(), + /** + * Enables\disabled various debugging features + */ + debug: z + .object({ + /** + * If set to true, newly initiated particle ids will be printed to console. + * Useful to see what particle id is responsible for aqua function + */ + printParticleId: z.boolean(), + }) + .partial(), + }) + .partial(); + /** * Configuration used when initiating Fluence Client */ -export interface ClientConfig { - /** - * Specify the KeyPair to be used to identify the Fluence Peer. - * Will be generated randomly if not specified - */ - keyPair?: KeyPairOptions; - - /** - * Options to configure the connection to the Fluence network - */ - connectionOptions?: { - /** - * When the peer established the connection to the network it sends a ping-like message to check if it works correctly. - * The options allows to specify the timeout for that message in milliseconds. - * If not specified the default timeout will be used - */ - skipCheckConnection?: boolean; - - /** - * The dialing timeout in milliseconds - */ - dialTimeoutMs?: number; - - /** - * The maximum number of inbound streams for the libp2p node. - * Default: 1024 - */ - maxInboundStreams?: number; - - /** - * The maximum number of outbound streams for the libp2p node. - * Default: 1024 - */ - maxOutboundStreams?: number; - }; - - /** - * Sets the default TTL for all particles originating from the peer with no TTL specified. - * If the originating particle's TTL is defined then that value will be used - * If the option is not set default TTL will be 7000 - */ - defaultTtlMs?: number; - - /** - * Property for passing custom CDN Url to load dependencies from browser. https://unpkg.com used by default - */ - CDNUrl?: string; - - /** - * Enables\disabled various debugging features - */ - debug?: { - /** - * If set to true, newly initiated particle ids will be printed to console. - * Useful to see what particle id is responsible for aqua function - */ - printParticleId?: boolean; - }; -} +export type ClientConfig = z.infer; diff --git a/packages/core/js-client/src/compilerSupport/callFunction.ts b/packages/core/js-client/src/compilerSupport/callFunction.ts index 66552cc0..6b2a03ad 100644 --- a/packages/core/js-client/src/compilerSupport/callFunction.ts +++ b/packages/core/js-client/src/compilerSupport/callFunction.ts @@ -61,7 +61,6 @@ export const callAquaFunction = async ({ peer, args, }: CallAquaFunctionArgs) => { - // TODO: this function should be rewritten. We can remove asserts if we wont check definition there log.trace("calling aqua function %j", { script, config, args }); const particle = await peer.internals.createNewParticle(script, config.ttl); @@ -88,15 +87,6 @@ export const callAquaFunction = async ({ // 1. The particle is sent to the network (state 'sent') // 2. All CallRequests are executed, e.g., all variable loading and local function calls are completed (state 'localWorkDone') - // TODO: make test - // if ( - // isReturnTypeVoid(def) && - // (stage.stage === "sent" || stage.stage === "localWorkDone") - // ) { - // resolve(undefined); - // } - // }, - peer.internals.initiateParticle(particle, resolve, reject); }); }; diff --git a/packages/core/js-client/src/compilerSupport/conversions.ts b/packages/core/js-client/src/compilerSupport/conversions.ts deleted file mode 100644 index 43e26186..00000000 --- a/packages/core/js-client/src/compilerSupport/conversions.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * Copyright 2023 Fluence Labs Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// TODO: This file is a mess. Need to refactor it later -/* eslint-disable */ -// @ts-nocheck - -import assert from "assert"; - -import type { - ArrowType, - ArrowWithoutCallbacks, - JSONArray, - JSONValue, - LabeledProductType, - NonArrowType, - SimpleTypes, -} from "@fluencelabs/interfaces"; -import { match } from "ts-pattern"; - -import { CallServiceData } from "../jsServiceHost/interfaces.js"; -import { jsonify } from "../util/utils.js"; - -/** - * Convert value from its representation in aqua language to representation in typescript - * @param value - value as represented in aqua - * @param type - definition of the aqua type - * @returns value represented in typescript - */ -export const aqua2ts = (value: JSONValue, type: NonArrowType): JSONValue => { - const res = match(type) - .with({ tag: "nil" }, () => { - return null; - }) - .with({ tag: "option" }, (opt) => { - assert(Array.isArray(value), "Should not be possible, bad types"); - - if (value.length === 0) { - return null; - } else { - return aqua2ts(value[0], opt.type); - } - }) - .with({ tag: "scalar" }, { tag: "bottomType" }, { tag: "topType" }, () => { - return value; - }) - .with({ tag: "array" }, (arr) => { - assert(Array.isArray(value), "Should not be possible, bad types"); - return value.map((y) => { - return aqua2ts(y, arr.type); - }); - }) - .with({ tag: "struct" }, (x) => { - return Object.entries(x.fields).reduce((agg, [key, type]) => { - const val = aqua2ts(value[key], type); - return { ...agg, [key]: val }; - }, {}); - }) - .with({ tag: "labeledProduct" }, (x) => { - return Object.entries(x.fields).reduce((agg, [key, type]) => { - const val = aqua2ts(value[key], type); - return { ...agg, [key]: val }; - }, {}); - }) - .with({ tag: "unlabeledProduct" }, (x) => { - return x.items.map((type, index) => { - return aqua2ts(value[index], type); - }); - }) - // uncomment to check that every pattern in matched - // .exhaustive(); - .otherwise(() => { - throw new Error("Unexpected tag: " + jsonify(type)); - }); - - return res; -}; - -/** - * Convert call service arguments list from their aqua representation to representation in typescript - * @param req - call service data - * @param arrow - aqua type definition - * @returns arguments in typescript representation - */ -export const aquaArgs2Ts = ( - req: CallServiceData, - arrow: ArrowType>, -): JSONArray => { - const argTypes = match(arrow.domain) - .with({ tag: "labeledProduct" }, (x) => { - return Object.values(x.fields); - }) - .with({ tag: "unlabeledProduct" }, (x) => { - return x.items; - }) - .with({ tag: "nil" }, (x) => { - return []; - }) - // uncomment to check that every pattern in matched - // .exhaustive() - .otherwise(() => { - throw new Error("Unexpected tag: " + jsonify(arrow.domain)); - }); - - if (req.args.length !== argTypes.length) { - throw new Error( - `incorrect number of arguments, expected: ${argTypes.length}, got: ${req.args.length}`, - ); - } - - return req.args.map((arg, index) => { - return aqua2ts(arg, argTypes[index]); - }); -}; - -/** - * Convert value from its typescript representation to representation in aqua - * @param value - the value as represented in typescript - * @param type - definition of the aqua type - * @returns value represented in aqua - */ -export const ts2aqua = (value: JSONValue, type: NonArrowType): JSONValue => { - const res = match(type) - .with({ tag: "nil" }, () => { - return null; - }) - .with({ tag: "option" }, (opt) => { - if (value === null || value === undefined) { - return []; - } else { - return [ts2aqua(value, opt.type)]; - } - }) - .with({ tag: "scalar" }, { tag: "bottomType" }, { tag: "topType" }, () => { - return value; - }) - .with({ tag: "array" }, (arr) => { - assert(Array.isArray(value), "Should not be possible, bad types"); - return value.map((y) => { - return ts2aqua(y, arr.type); - }); - }) - .with({ tag: "struct" }, (x) => { - return Object.entries(x.fields).reduce((agg, [key, type]) => { - const val = ts2aqua(value[key], type); - return { ...agg, [key]: val }; - }, {}); - }) - .with({ tag: "labeledProduct" }, (x) => { - return Object.entries(x.fields).reduce((agg, [key, type]) => { - const val = ts2aqua(value[key], type); - return { ...agg, [key]: val }; - }, {}); - }) - .with({ tag: "unlabeledProduct" }, (x) => { - return x.items.map((type, index) => { - return ts2aqua(value[index], type); - }); - }) - // uncomment to check that every pattern in matched - // .exhaustive() - .otherwise(() => { - throw new Error("Unexpected tag: " + jsonify(type)); - }); - - return res; -}; - -/** - * Convert return type of the service from it's typescript representation to representation in aqua - * @param returnValue - the value as represented in typescript - * @param arrowType - the arrow type which describes the service - * @returns - value represented in aqua - */ -export const returnType2Aqua = ( - returnValue: any, - arrowType: ArrowType, -) => { - // TODO: cover with tests - if (arrowType.codomain.tag === "nil") { - return {}; - } - - if (arrowType.codomain.items.length === 0) { - return {}; - } - - if (arrowType.codomain.items.length === 1) { - return ts2aqua(returnValue, arrowType.codomain.items[0]); - } - - return arrowType.codomain.items.map((type, index) => { - return ts2aqua(returnValue[index], type); - }); -}; - -/** - * Converts response value from aqua its representation to representation in typescript - * @param req - call service data - * @param arrow - aqua type definition - * @returns response value in typescript representation - */ -export const responseServiceValue2ts = ( - req: CallServiceData, - arrow: ArrowType, -) => { - return match(arrow.codomain) - .with({ tag: "nil" }, () => { - return null; - }) - .with({ tag: "unlabeledProduct" }, (x) => { - if (x.items.length === 0) { - return null; - } - - if (x.items.length === 1) { - return aqua2ts(req.args[0], x.items[0]); - } - - return req.args.map((y, index) => { - return aqua2ts(y, x.items[index]); - }); - }) - .exhaustive(); -}; diff --git a/packages/core/js-client/src/index.ts b/packages/core/js-client/src/index.ts index be8e8839..6517d84f 100644 --- a/packages/core/js-client/src/index.ts +++ b/packages/core/js-client/src/index.ts @@ -16,12 +16,15 @@ import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher"; import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver"; +import { ZodError } from "zod"; import { ClientPeer, makeClientPeerConfig } from "./clientPeer/ClientPeer.js"; import { ClientConfig, + configSchema, ConnectionState, RelayOptions, + relaySchema, } from "./clientPeer/types.js"; import { callAquaFunction } from "./compilerSupport/callFunction.js"; import { registerService } from "./compilerSupport/registerService.js"; @@ -33,6 +36,15 @@ const createClient = async ( relay: RelayOptions, config: ClientConfig = {}, ): Promise => { + try { + relay = relaySchema.parse(relay); + config = configSchema.parse(config); + } catch (e) { + if (e instanceof ZodError) { + throw new Error(JSON.stringify(e.format())); + } + } + const CDNUrl = config.CDNUrl ?? DEFAULT_CDN_URL; const fetchMarineJsWasm = async () => { diff --git a/packages/core/js-client/src/services/__test__/builtInHandler.spec.ts b/packages/core/js-client/src/services/__test__/builtInHandler.spec.ts index 39b1ba41..e942301d 100644 --- a/packages/core/js-client/src/services/__test__/builtInHandler.spec.ts +++ b/packages/core/js-client/src/services/__test__/builtInHandler.spec.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import assert from "assert"; - import { JSONArray } from "@fluencelabs/interfaces"; import { toUint8Array } from "js-base64"; import { it, describe, expect, test } from "vitest"; @@ -28,6 +26,7 @@ import { KeyPair } from "../../keypair/index.js"; import { builtInServices } from "../builtins.js"; import { allowServiceFn } from "../securityGuard.js"; import { Sig, defaultSigGuard } from "../Sig.js"; +import assert from "assert"; const a10b20 = `{ "a": 10, @@ -54,32 +53,32 @@ describe("Tests for default handler", () => { serviceId | fnName | args | retCode | result ${"op"} | ${"identity"} | ${[]} | ${0} | ${{}} ${"op"} | ${"identity"} | ${[1]} | ${0} | ${1} - ${"op"} | ${"identity"} | ${[1, 2]} | ${1} | ${"identity accepts up to 1 arguments, received 2 arguments"} + ${"op"} | ${"identity"} | ${[1, 2]} | ${1} | ${"Expected 1 argument(s). Got 2"} ${"op"} | ${"noop"} | ${[1, 2]} | ${0} | ${{}} ${"op"} | ${"array"} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]} ${"op"} | ${"array_length"} | ${[[1, 2, 3]]} | ${0} | ${3} - ${"op"} | ${"array_length"} | ${[]} | ${1} | ${"array_length accepts exactly one argument, found: 0"} + ${"op"} | ${"array_length"} | ${[]} | ${1} | ${"Expected 1 argument(s). Got 0"} ${"op"} | ${"concat"} | ${[[1, 2], [3, 4], [5, 6]]} | ${0} | ${[1, 2, 3, 4, 5, 6]} ${"op"} | ${"concat"} | ${[[1, 2]]} | ${0} | ${[1, 2]} ${"op"} | ${"concat"} | ${[]} | ${0} | ${[]} - ${"op"} | ${"concat"} | ${[1, [1, 2], 1]} | ${1} | ${"All arguments of 'concat' must be arrays: arguments 0, 2 are not"} + ${"op"} | ${"concat"} | ${[1, [1, 2], 1]} | ${1} | ${"Argument 0 expected to be of type array, Got number"} ${"op"} | ${"string_to_b58"} | ${["test"]} | ${0} | ${"3yZe7d"} - ${"op"} | ${"string_to_b58"} | ${["test", 1]} | ${1} | ${"string_to_b58 accepts only one string argument"} + ${"op"} | ${"string_to_b58"} | ${["test", 1]} | ${1} | ${"Expected 1 argument(s). Got 2"} ${"op"} | ${"string_from_b58"} | ${["3yZe7d"]} | ${0} | ${"test"} - ${"op"} | ${"string_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"string_from_b58 accepts only one string argument"} + ${"op"} | ${"string_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"Expected 1 argument(s). Got 2"} ${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116]]} | ${0} | ${"3yZe7d"} - ${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116], 1]} | ${1} | ${"bytes_to_b58 accepts only single argument: array of numbers"} + ${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116], 1]} | ${1} | ${"Expected 1 argument(s). Got 2"} ${"op"} | ${"bytes_from_b58"} | ${["3yZe7d"]} | ${0} | ${[116, 101, 115, 116]} - ${"op"} | ${"bytes_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"bytes_from_b58 accepts only one string argument"} + ${"op"} | ${"bytes_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"Expected 1 argument(s). Got 2"} ${"op"} | ${"sha256_string"} | ${["hello, world!"]} | ${0} | ${"QmVQ8pg6L1tpoWYeq6dpoWqnzZoSLCh7E96fCFXKvfKD3u"} - ${"op"} | ${"sha256_string"} | ${["hello, world!", true]} | ${1} | ${"sha256_string accepts 1 argument, found: 2"} - ${"op"} | ${"sha256_string"} | ${[]} | ${1} | ${"sha256_string accepts 1 argument, found: 0"} + ${"op"} | ${"sha256_string"} | ${["hello, world!", true]} | ${1} | ${"Expected 1 argument(s). Got 2"} + ${"op"} | ${"sha256_string"} | ${[]} | ${1} | ${"Expected 1 argument(s). Got 0"} ${"op"} | ${"concat_strings"} | ${[]} | ${0} | ${""} ${"op"} | ${"concat_strings"} | ${["a", "b", "c"]} | ${0} | ${"abc"} - ${"peer"} | ${"timeout"} | ${[200, []]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"} + ${"peer"} | ${"timeout"} | ${[200, []]} | ${1} | ${"Argument 1 expected to be of type string, Got array"} ${"peer"} | ${"timeout"} | ${[200, "test"]} | ${0} | ${"test"} - ${"peer"} | ${"timeout"} | ${[]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"} - ${"peer"} | ${"timeout"} | ${[200, "test", 1]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"} + ${"peer"} | ${"timeout"} | ${[]} | ${1} | ${"Expected 2 argument(s). Got 0"} + ${"peer"} | ${"timeout"} | ${[200, "test", 1]} | ${1} | ${"Expected 2 argument(s). Got 3"} ${"debug"} | ${"stringify"} | ${[]} | ${0} | ${'""'} ${"debug"} | ${"stringify"} | ${[{ a: 10, b: 20 }]} | ${0} | ${a10b20} ${"debug"} | ${"stringify"} | ${[1, 2, 3, 4]} | ${0} | ${oneTwoThreeFour} diff --git a/packages/core/js-client/src/services/__test__/sigService.spec.ts b/packages/core/js-client/src/services/__test__/sigService.spec.ts index 1a8f8736..ed0ade8b 100644 --- a/packages/core/js-client/src/services/__test__/sigService.spec.ts +++ b/packages/core/js-client/src/services/__test__/sigService.spec.ts @@ -20,7 +20,6 @@ import * as url from "url"; import { it, describe, expect, beforeAll } from "vitest"; import { registerService } from "../../compilerSupport/registerService.js"; -import { ServiceImpl } from "../../compilerSupport/types.js"; import { KeyPair } from "../../keypair/index.js"; import { compileAqua, CompiledFnCall, withPeer } from "../../util/testUtils.js"; import { allowServiceFn } from "../securityGuard.js"; @@ -48,12 +47,12 @@ describe("Sig service test suite", () => { const customSig = new Sig(customKeyPair); const data = [1, 2, 3, 4, 5]; + const anyService: Record = customSig; + registerService({ peer, serviceId: "CustomSig", - // TODO: fix this after changing registerService signature - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - service: customSig as unknown as ServiceImpl, + service: anyService, }); registerService({ @@ -89,12 +88,12 @@ describe("Sig service test suite", () => { const customSig = new Sig(customKeyPair); const data = [1, 2, 3, 4, 5]; + const anyService: Record = customSig; + registerService({ peer, serviceId: "CustomSig", - // TODO: fix this after changing registerService signature - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - service: customSig as unknown as ServiceImpl, + service: anyService, }); registerService({ diff --git a/packages/core/js-client/src/services/_aqua/node-utils.ts b/packages/core/js-client/src/services/_aqua/node-utils.ts index e5b11d47..ebdd8d9f 100644 --- a/packages/core/js-client/src/services/_aqua/node-utils.ts +++ b/packages/core/js-client/src/services/_aqua/node-utils.ts @@ -15,7 +15,6 @@ */ import { registerService } from "../../compilerSupport/registerService.js"; -import { ServiceImpl } from "../../compilerSupport/types.js"; import { FluencePeer } from "../../jsPeer/FluencePeer.js"; import { NodeUtils } from "../NodeUtils.js"; @@ -24,11 +23,11 @@ export function registerNodeUtils( serviceId: string, service: NodeUtils, ) { + const anyService: Record = service; + registerService({ peer, - // TODO: fix this after changing registerService signature - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - service: service as unknown as ServiceImpl, + service: anyService, serviceId, }); } diff --git a/packages/core/js-client/src/services/_aqua/services.ts b/packages/core/js-client/src/services/_aqua/services.ts index 561fb78d..1afce610 100644 --- a/packages/core/js-client/src/services/_aqua/services.ts +++ b/packages/core/js-client/src/services/_aqua/services.ts @@ -15,7 +15,6 @@ */ import { registerService } from "../../compilerSupport/registerService.js"; -import { ServiceImpl } from "../../compilerSupport/types.js"; import { FluencePeer } from "../../jsPeer/FluencePeer.js"; import { ParticleContext } from "../../jsServiceHost/interfaces.js"; import { Sig } from "../Sig.js"; @@ -46,11 +45,11 @@ export function registerSig( serviceId: string, service: Sig, ) { + const anyService: Record = service; + registerService({ peer, - // TODO: fix this after changing registerService signature - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - service: service as unknown as ServiceImpl, + service: anyService, serviceId, }); } diff --git a/packages/core/js-client/src/services/_aqua/single-module-srv.ts b/packages/core/js-client/src/services/_aqua/single-module-srv.ts index 1dc90dc6..a5443424 100644 --- a/packages/core/js-client/src/services/_aqua/single-module-srv.ts +++ b/packages/core/js-client/src/services/_aqua/single-module-srv.ts @@ -15,7 +15,6 @@ */ import { registerService } from "../../compilerSupport/registerService.js"; -import { ServiceImpl } from "../../compilerSupport/types.js"; import { FluencePeer } from "../../jsPeer/FluencePeer.js"; import { Srv } from "../SingleModuleSrv.js"; @@ -24,12 +23,12 @@ export function registerSrv( serviceId: string, service: Srv, ) { + const anyService: Record = service; + registerService({ peer, serviceId, - // TODO: fix this after changing registerService signature - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - service: service as unknown as ServiceImpl, + service: anyService, }); } diff --git a/packages/core/js-client/src/services/builtins.ts b/packages/core/js-client/src/services/builtins.ts index 7ee3f68d..b6541fa8 100644 --- a/packages/core/js-client/src/services/builtins.ts +++ b/packages/core/js-client/src/services/builtins.ts @@ -14,74 +14,142 @@ * limitations under the License. */ -import assert from "assert"; import { Buffer } from "buffer"; import { JSONValue } from "@fluencelabs/interfaces"; import bs58 from "bs58"; import { sha256 } from "multiformats/hashes/sha2"; +import { z } from "zod"; import { + CallServiceData, CallServiceResult, CallServiceResultType, GenericCallServiceHandler, ResultCodes, } from "../jsServiceHost/interfaces.js"; -import { getErrorMessage, isString, jsonify } from "../util/utils.js"; +import { getErrorMessage, jsonify } from "../util/utils.js"; -const success = ( - // TODO: Remove unknown after adding validation to builtin inputs - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - result: CallServiceResultType | unknown, -): CallServiceResult => { +const success = (result: CallServiceResultType): CallServiceResult => { return { - // TODO: Remove type assertion after adding validation to builtin inputs - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - result: result as CallServiceResultType, + result, retCode: ResultCodes.success, }; }; -const error = ( - // TODO: Remove unknown after adding validation to builtin inputs - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - error: CallServiceResultType | unknown, -): CallServiceResult => { +const error = (error: CallServiceResultType): CallServiceResult => { return { - // TODO: Remove type assertion after adding validation to builtin inputs - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - result: error as CallServiceResultType, + result: error, retCode: ResultCodes.error, }; }; +const chunk = (arr: T[]): T[][] => { + const res: T[][] = []; + const chunkSize = 2; + + for (let i = 0; i < arr.length; i += chunkSize) { + const chunk = arr.slice(i, i + chunkSize); + res.push(chunk); + } + + return res; +}; + const errorNotImpl = (methodName: string) => { return error( `The JS implementation of Peer does not support "${methodName}"`, ); }; -const makeJsonImpl = (args: [Record, ...JSONValue[]]) => { - const [obj, ...kvs] = args; +const parseWithSchema = ( + schema: T, + req: CallServiceData, +): [z.infer, null] | [null, string] => { + const result = schema.safeParse(req.args, { + errorMap: (issue, ctx) => { + if (issue.code === z.ZodIssueCode.invalid_type) { + if (issue.path.length === 1 && typeof issue.path[0] === "number") { + const [arg] = issue.path; + return { + message: `Argument ${arg} expected to be of type ${issue.expected}, Got ${issue.received}`, + }; + } + } - const toMerge: Record = {}; + if (issue.code === z.ZodIssueCode.too_big) { + return { + message: `Expected ${ + issue.maximum + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } argument(s). Got ${ctx.data.length}`, + }; + } - for (let i = 0; i < kvs.length / 2; i++) { - const k = kvs[i * 2]; + if (issue.code === z.ZodIssueCode.too_small) { + return { + message: `Expected ${ + issue.minimum + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } argument(s). Got ${ctx.data.length}`, + }; + } - if (!isString(k)) { - return error(`Argument ${i * 2 + 1} is expected to be string`); - } + if (issue.code === z.ZodIssueCode.invalid_union) { + return { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + message: `Expected argument(s). Got ${ctx.data.length}`, + }; + } - const v = kvs[i * 2 + 1]; - toMerge[k] = v; + return { message: ctx.defaultError }; + }, + }); + + if (result.success) { + return [result.data, null]; + } else { + return [null, result.error.errors[0].message]; } - - const res = { ...obj, ...toMerge }; - return success(res); }; -// TODO: These assert made for silencing more stricter ts rules. Will be fixed in DXJ-493 +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; + +const jsonSchema: z.ZodType = z.lazy(() => { + return z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]); +}); + +const jsonImplSchema = z + .tuple([z.record(jsonSchema)]) + .rest(z.tuple([z.string(), jsonSchema])); + +const makeJsonImpl = (args: z.infer) => { + const [obj, ...kvs] = args; + return success({ ...obj, ...Object.fromEntries(kvs) }); +}; + +type withSchema = ( + arg: T, +) => ( + arg1: (value: z.infer) => CallServiceResult | Promise, +) => (req: CallServiceData) => CallServiceResult | Promise; + +const withSchema: withSchema = (schema: T) => { + return (bound) => { + return (req) => { + const [value, message] = parseWithSchema(schema, req); + + if (message != null) { + return error(message); + } + + return bound(value); + }; + }; +}; + export const builtInServices: Record< string, Record @@ -116,29 +184,16 @@ export const builtInServices: Record< return errorNotImpl("peer.get_contact"); }, - timeout: (req) => { - if (req.args.length !== 2) { - return error( - "timeout accepts exactly two arguments: timeout duration in ms and a message string", - ); - } - - const durationMs = req.args[0]; - const message = req.args[1]; - - if (typeof durationMs !== "number" || typeof message !== "string") { - return error( - "timeout accepts exactly two arguments: timeout duration in ms and a message string", - ); - } - - return new Promise((resolve) => { - setTimeout(() => { - const res = success(message); - resolve(res); - }, durationMs); - }); - }, + timeout: withSchema(z.tuple([z.number(), z.string()]))( + ([durationMs, msg]) => { + return new Promise((resolve) => { + setTimeout(() => { + const res = success(msg); + resolve(res); + }, durationMs); + }); + }, + ), }, kad: { @@ -246,120 +301,48 @@ export const builtInServices: Record< return success(req.args); }, - array_length: (req) => { - if (req.args.length !== 1) { - return error( - "array_length accepts exactly one argument, found: " + - req.args.length, - ); - } else { - assert(Array.isArray(req.args[0])); - return success(req.args[0].length); - } - }, + array_length: withSchema(z.tuple([z.array(z.unknown())]))(([arr]) => { + return success(arr.length); + }), - identity: (req) => { - if (req.args.length > 1) { - return error( - `identity accepts up to 1 arguments, received ${req.args.length} arguments`, - ); - } else { - return success(req.args.length === 0 ? {} : req.args[0]); - } - }, + identity: withSchema(z.array(jsonSchema).max(1))((args) => { + return success(args.length === 0 ? {} : args[0]); + }), - concat: (req) => { - const incorrectArgIndices = req.args // - .map((x, i): [boolean, number] => { - return [Array.isArray(x), i]; - }) - .filter(([isArray]) => { - return !isArray; - }) - .map(([, index]) => { - return index; - }); - - if (incorrectArgIndices.length > 0) { - const str = incorrectArgIndices.join(", "); - return error( - `All arguments of 'concat' must be arrays: arguments ${str} are not`, - ); - } else { - // TODO: remove after adding validation - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return success([].concat(...(req.args as never[][]))); - } - }, - - string_to_b58: (req) => { - if (req.args.length !== 1) { - return error("string_to_b58 accepts only one string argument"); - } else { - const [input] = req.args; - // TODO: remove after adding validation - assert(typeof input === "string"); - return success(bs58.encode(new TextEncoder().encode(input))); - } - }, - - string_from_b58: (req) => { - if (req.args.length !== 1) { - return error("string_from_b58 accepts only one string argument"); - } else { - const [input] = req.args; - // TODO: remove after adding validation - assert(typeof input === "string"); - return success(new TextDecoder().decode(bs58.decode(input))); - } - }, - - bytes_to_b58: (req) => { - if (req.args.length !== 1 || !Array.isArray(req.args[0])) { - return error( - "bytes_to_b58 accepts only single argument: array of numbers", - ); - } else { - // TODO: remove after adding validation - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const argumentArray = req.args[0] as number[]; - return success(bs58.encode(new Uint8Array(argumentArray))); - } - }, - - bytes_from_b58: (req) => { - if (req.args.length !== 1) { - return error("bytes_from_b58 accepts only one string argument"); - } else { - const [input] = req.args; - // TODO: remove after adding validation - assert(typeof input === "string"); - return success(Array.from(bs58.decode(input))); - } - }, - - sha256_string: async (req) => { - if (req.args.length !== 1) { - return error( - `sha256_string accepts 1 argument, found: ${req.args.length}`, - ); - } else { - const [input] = req.args; - // TODO: remove after adding validation - assert(typeof input === "string"); - const inBuffer = Buffer.from(input); - const multihash = await sha256.digest(inBuffer); - - return success(bs58.encode(multihash.bytes)); - } - }, - - concat_strings: (req) => { - // TODO: remove after adding validation + concat: withSchema(z.array(z.array(z.unknown())))((args) => { + // concat accepts only 'never' type // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const res = "".concat(...(req.args as string[])); + const arr = args as never[][]; + return success([].concat(...arr)); + }), + + string_to_b58: withSchema(z.tuple([z.string()]))(([input]) => { + return success(bs58.encode(new TextEncoder().encode(input))); + }), + + string_from_b58: withSchema(z.tuple([z.string()]))(([input]) => { + return success(new TextDecoder().decode(bs58.decode(input))); + }), + + bytes_to_b58: withSchema(z.tuple([z.array(z.number())]))(([input]) => { + return success(bs58.encode(new Uint8Array(input))); + }), + + bytes_from_b58: withSchema(z.tuple([z.string()]))(([input]) => { + return success(Array.from(bs58.decode(input))); + }), + + sha256_string: withSchema(z.tuple([z.string()]))(async ([input]) => { + const inBuffer = Buffer.from(input); + const multihash = await sha256.digest(inBuffer); + + return success(bs58.encode(multihash.bytes)); + }), + + concat_strings: withSchema(z.array(z.string()))((args) => { + const res = "".concat(...args); return success(res); - }, + }), }, debug: { @@ -379,365 +362,187 @@ export const builtInServices: Record< }, math: { - add: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + add: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x + y); - }, + }), - sub: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + sub: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x - y); - }, + }), - mul: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + mul: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x * y); - }, + }), - fmul: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + fmul: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(Math.floor(x * y)); - }, + }), - div: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + div: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(Math.floor(x / y)); - }, + }), - rem: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + rem: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x % y); - }, + }), - pow: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + pow: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(Math.pow(x, y)); - }, + }), - log: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + log: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(Math.log(y) / Math.log(x)); - }, + }), }, cmp: { - gt: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + gt: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x > y); - }, + }), - gte: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + gte: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x >= y); - }, + }), - lt: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + lt: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x < y); - }, + }), - lte: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + lte: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x <= y); - }, + }), - cmp: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [x, y] = req.args; - // TODO: Remove after adding validation - assert(typeof x === "number" && typeof y === "number"); + cmp: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => { return success(x === y ? 0 : x > y ? 1 : -1); - }, + }), }, array: { - sum: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 1)) != null) { - return err; - } - - // TODO: Remove after adding validation - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const [xs] = req.args as [number[]]; + sum: withSchema(z.tuple([z.array(z.number())]))(([xs]) => { return success( xs.reduce((agg, cur) => { return agg + cur; }, 0), ); - }, + }), - dedup: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 1)) != null) { - return err; - } - - const [xs] = req.args; - // TODO: Remove after adding validation - assert(Array.isArray(xs)); + dedup: withSchema(z.tuple([z.array(z.any())]))(([xs]) => { const set = new Set(xs); return success(Array.from(set)); - }, + }), - intersect: (req) => { - let err; + intersect: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))( + ([xs, ys]) => { + const intersection = xs.filter((x) => { + return ys.includes(x); + }); - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } + return success(intersection); + }, + ), - const [xs, ys] = req.args; - // TODO: Remove after adding validation - assert(Array.isArray(xs) && Array.isArray(ys)); + diff: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))( + ([xs, ys]) => { + const diff = xs.filter((x) => { + return !ys.includes(x); + }); - const intersection = xs.filter((x) => { - return ys.includes(x); - }); + return success(diff); + }, + ), - return success(intersection); - }, + sdiff: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))( + ([xs, ys]) => { + const sdiff = [ + xs.filter((y) => { + return !ys.includes(y); + }), + ys.filter((x) => { + return !xs.includes(x); + }), + ].flat(); - diff: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [xs, ys] = req.args; - // TODO: Remove after adding validation - assert(Array.isArray(xs) && Array.isArray(ys)); - - const diff = xs.filter((x) => { - return !ys.includes(x); - }); - - return success(diff); - }, - - sdiff: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 2)) != null) { - return err; - } - - const [xs, ys] = req.args; - // TODO: Remove after adding validation - assert(Array.isArray(xs) && Array.isArray(ys)); - - const sdiff = [ - // force new line - ...xs.filter((y) => { - return !ys.includes(y); - }), - ...ys.filter((x) => { - return !xs.includes(x); - }), - ]; - - return success(sdiff); - }, + return success(sdiff); + }, + ), }, json: { - obj: (req) => { - let err; + obj: withSchema( + z + .array(z.unknown()) + .refine( + (arr) => { + return arr.length % 2 === 0; + }, + (arr) => { + return { + message: "Expected even number of argument(s). Got " + arr.length, + }; + }, + ) + .transform((args) => { + return chunk(args); + }) + .pipe(z.array(z.tuple([z.string(), jsonSchema]))), + )((args) => { + return makeJsonImpl([{}, ...args]); + }), - if ((err = checkForArgumentsCountEven(req)) != null) { - return err; - } + put: withSchema( + z + .tuple([z.record(jsonSchema), z.string(), jsonSchema]) + .transform( + ([obj, name, value]): [{ [key: string]: Json }, [string, Json]] => { + return [obj, [name, value]]; + }, + ), + )(makeJsonImpl), - // TODO: remove after adding validation - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return makeJsonImpl([{}, ...req.args] as [ - Record, - ...JSONValue[], - ]); - }, + puts: withSchema( + z + .array(z.unknown()) + .refine( + (arr) => { + return arr.length >= 3; + }, + (value) => { + return { + message: `Expected more than 3 argument(s). Got ${value.length}`, + }; + }, + ) + .refine( + (arr) => { + return arr.length % 2 === 1; + }, + { + message: "Argument count must be odd.", + }, + ) + .transform((args) => { + return [args[0], ...chunk(args.slice(1))]; + }) + .pipe(jsonImplSchema), + )(makeJsonImpl), - put: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 3)) != null) { - return err; - } - - if ((err = checkForArgumentType(req, 0, "object")) != null) { - return err; - } - - return makeJsonImpl( - // TODO: remove after adding validation - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - req.args as [Record, ...JSONValue[]], - ); - }, - - puts: (req) => { - let err; - - if ((err = checkForArgumentsCountOdd(req)) != null) { - return err; - } - - if ((err = checkForArgumentsCountMoreThan(req, 3)) != null) { - return err; - } - - if ((err = checkForArgumentType(req, 0, "object")) != null) { - return err; - } - - return makeJsonImpl( - // TODO: remove after adding validation - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - req.args as [Record, ...JSONValue[]], - ); - }, - - stringify: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 1)) != null) { - return err; - } - - if ((err = checkForArgumentType(req, 0, "object")) != null) { - return err; - } - - const [json] = req.args; - const res = JSON.stringify(json); - return success(res); - }, - - parse: (req) => { - let err; - - if ((err = checkForArgumentsCount(req, 1)) != null) { - return err; - } - - if ((err = checkForArgumentType(req, 0, "string")) != null) { - return err; - } - - const [raw] = req.args; + stringify: withSchema(z.tuple([z.record(z.string(), jsonSchema)]))( + ([json]) => { + const res = JSON.stringify(json); + return success(res); + }, + ), + parse: withSchema(z.tuple([z.string()]))(([raw]) => { try { - // TODO: Remove after adding validation - assert(typeof raw === "string"); - const json = JSON.parse(raw); + // Parsing any argument here yields JSONValue + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const json = JSON.parse(raw) as JSONValue; return success(json); } catch (err: unknown) { return error(getErrorMessage(err)); } - }, + }), }, "run-console": { @@ -749,59 +554,3 @@ export const builtInServices: Record< }, }, } as const; - -const checkForArgumentsCount = ( - req: { args: Array }, - count: number, -) => { - if (req.args.length !== count) { - return error(`Expected ${count} argument(s). Got ${req.args.length}`); - } - - return null; -}; - -const checkForArgumentsCountMoreThan = ( - req: { args: Array }, - count: number, -) => { - if (req.args.length < count) { - return error( - `Expected more than ${count} argument(s). Got ${req.args.length}`, - ); - } - - return null; -}; - -const checkForArgumentsCountEven = (req: { args: Array }) => { - if (req.args.length % 2 === 1) { - return error(`Expected even number of argument(s). Got ${req.args.length}`); - } - - return null; -}; - -const checkForArgumentsCountOdd = (req: { args: Array }) => { - if (req.args.length % 2 === 0) { - return error(`Expected odd number of argument(s). Got ${req.args.length}`); - } - - return null; -}; - -const checkForArgumentType = ( - req: { args: Array }, - index: number, - type: string, -) => { - const actual = typeof req.args[index]; - - if (actual !== type) { - return error( - `Argument ${index} expected to be of type ${type}, Got ${actual}`, - ); - } - - return null; -}; diff --git a/packages/core/js-client/src/test.ts b/packages/core/js-client/src/test.ts new file mode 100644 index 00000000..19a0866f --- /dev/null +++ b/packages/core/js-client/src/test.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2023 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { classToPlain } from "class-transformer"; + +import { NodeUtils } from "./services/NodeUtils.js"; + +import { Fluence } from "./index.js"; + +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +console.log(classToPlain(new NodeUtils(Fluence.defaultClient!))); +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +console.log( + Object.getPrototypeOf(classToPlain(new NodeUtils(Fluence.defaultClient!))), +); diff --git a/packages/core/marine-worker/package.json b/packages/core/marine-worker/package.json index acb6e462..d5e3f5c2 100644 --- a/packages/core/marine-worker/package.json +++ b/packages/core/marine-worker/package.json @@ -24,7 +24,7 @@ "vitest": "0.34.6" }, "dependencies": { - "@fluencelabs/marine-js": "0.7.2", + "@fluencelabs/marine-js": "0.8.0", "observable-fns": "0.6.1", "@fluencelabs/threads": "^2.0.0" } diff --git a/packages/core/marine-worker/src/index.ts b/packages/core/marine-worker/src/index.ts index cf3c60fb..44569f8e 100644 --- a/packages/core/marine-worker/src/index.ts +++ b/packages/core/marine-worker/src/index.ts @@ -28,7 +28,6 @@ import type { } from "@fluencelabs/marine-js/dist/types"; import { defaultCallParameters, - JSONValue, logLevelToEnv, } from "@fluencelabs/marine-js/dist/types"; import { expose } from "@fluencelabs/threads/worker"; @@ -140,9 +139,7 @@ const toExpose = { throw new Error(`service with id=${serviceId} not found`); } - // TODO: Make MarineService return JSONValue type - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return srv.call(functionName, args, callParams) as JSONValue; + return srv.call(functionName, args, callParams); }, onLogMessage() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d144afb9..c107d624 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,6 +190,9 @@ importers: vitest: specifier: 0.34.6 version: 0.34.6 + zod: + specifier: 3.22.4 + version: 3.22.4 packages/core/interfaces: devDependencies: @@ -244,9 +247,6 @@ importers: '@multiformats/multiaddr': specifier: 11.3.0 version: 11.3.0 - assert: - specifier: 2.1.0 - version: 2.1.0 async: specifier: 3.2.4 version: 3.2.4 @@ -256,6 +256,9 @@ importers: buffer: specifier: 6.0.3 version: 6.0.3 + class-transformer: + specifier: 0.5.1 + version: 0.5.1 debug: specifier: 4.3.4 version: 4.3.4 @@ -289,13 +292,16 @@ importers: zod: specifier: 3.22.4 version: 3.22.4 + zod-validation-error: + specifier: 2.1.0 + version: 2.1.0(zod@3.22.4) devDependencies: '@fluencelabs/aqua-api': specifier: 0.9.3 version: 0.9.3 '@fluencelabs/marine-js': - specifier: 0.7.2 - version: 0.7.2 + specifier: 0.8.0 + version: 0.8.0 '@rollup/plugin-inject': specifier: 5.0.3 version: 5.0.3 @@ -333,8 +339,8 @@ importers: specifier: 0.54.0 version: 0.54.0 '@fluencelabs/marine-js': - specifier: 0.7.2 - version: 0.7.2 + specifier: 0.8.0 + version: 0.8.0 '@fluencelabs/marine-worker': specifier: 0.4.2 version: link:../marine-worker @@ -345,8 +351,8 @@ importers: packages/core/marine-worker: dependencies: '@fluencelabs/marine-js': - specifier: 0.7.2 - version: 0.7.2 + specifier: 0.8.0 + version: 0.8.0 '@fluencelabs/threads': specifier: ^2.0.0 version: 2.0.0 @@ -3714,8 +3720,8 @@ packages: /@fluencelabs/avm@0.54.0: resolution: {integrity: sha512-5GgROVly/vC7gasltr6/3TIY8vfV6b+SPfWUAGWnyXdbWt4jJANLO2YtXdaUsdNk9PiwOep7TMjLnypljdyMjQ==} - /@fluencelabs/marine-js@0.7.2: - resolution: {integrity: sha512-etjbXDgzyZkK82UZvtuIU3bfy5f52siDUy1m+T5Y5r70k82xYdZZ8vgWVgB6ivi2f3aDyQjgNTfzWQjKFpAReQ==} + /@fluencelabs/marine-js@0.8.0: + resolution: {integrity: sha512-exxp0T0Dk69dxnbpAiVc/qp66s8Jq/P71TRB9aeQZLZy3EQtVAMCBJvwQY8LzVVlYEyVjmqQkFG/N0rAeYU1vg==} dependencies: '@wasmer/wasi': 0.12.0 '@wasmer/wasmfs': 0.12.0 @@ -5910,16 +5916,6 @@ packages: util: 0.12.5 dev: true - /assert@2.1.0: - resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} - dependencies: - call-bind: 1.0.2 - is-nan: 1.3.2 - object-is: 1.1.5 - object.assign: 4.1.4 - util: 0.12.5 - dev: false - /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true @@ -6675,6 +6671,10 @@ packages: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: false + /class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + dev: false + /clean-css@5.3.2: resolution: {integrity: sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==} engines: {node: '>= 10.0'} @@ -9351,6 +9351,7 @@ packages: engines: {node: '>= 0.4'} dependencies: has-tostringtag: 1.0.0 + dev: true /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -9376,6 +9377,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.2.0 + dev: true /is-negative-zero@2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} @@ -14375,6 +14377,7 @@ packages: is-generator-function: 1.0.10 is-typed-array: 1.1.12 which-typed-array: 1.1.11 + dev: true /utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} @@ -15218,6 +15221,14 @@ packages: engines: {node: '>=12.20'} dev: true + /zod-validation-error@2.1.0(zod@3.22.4): + resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + dependencies: + zod: 3.22.4 + dev: false + /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - dev: false