diff --git a/integration-tests/.mocharc.json b/integration-tests/.mocharc.json new file mode 100644 index 0000000..e46db72 --- /dev/null +++ b/integration-tests/.mocharc.json @@ -0,0 +1,6 @@ +{ + "require": ["ts-node/register", "src/tests/root-hooks.ts"], + "extensions": ["ts"], + "spec": ["src/tests/**/*.test.ts"], + "timeout": 120000 +} diff --git a/integration-tests/INTEGRATION-TESTS-PLAN-PROMPT.md b/integration-tests/INTEGRATION-TESTS-PLAN-PROMPT.md new file mode 100644 index 0000000..42be9cf --- /dev/null +++ b/integration-tests/INTEGRATION-TESTS-PLAN-PROMPT.md @@ -0,0 +1,99 @@ + # Integration Tests Plan for ndc-nodejs-lambda + + ## Overview + Add integration tests that test the ndc-nodejs-lambda connector with a local DDN project. The tests exercise the full connector lifecycle: DDN project initialization, connector startup, NDC endpoint verification, DDN introspection, and + supergraph build. + + ## Directory Structure + ``` + integration-tests/ + ├── package.json # Test package with local SDK reference + ├── tsconfig.json + ├── .mocharc.json # Mocha config (120s timeout) + ├── fixtures/ + │ └── functions.ts # Test functions covering all type variations + └── src/ + ├── helpers/ + │ ├── connector-server.ts # Spawn/stop connector process, health polling + │ ├── http-client.ts # Typed NDC HTTP client (uses Node.js fetch) + │ ├── ddn-project.ts # DDN CLI wrappers (init, introspect, build) + │ └── temp-dir.ts # Temp directory create/cleanup + └── tests/ + ├── root-hooks.ts # Mocha root hooks: setup DDN project + start server + ├── health.test.ts + ├── capabilities.test.ts + ├── schema.test.ts # Validate NDC schema for all fixture functions + ├── query.test.ts # Test queries: scalars, objects, arrays, async, variables + ├── mutation.test.ts # Test mutations: procedures, state, async + ├── error-handling.test.ts # Forbidden/Conflict/Unprocessable/500 errors + ├── ddn-introspect.test.ts # DDN connector introspect against running server + └── ddn-build.test.ts # DDN supergraph build local + ``` + + ## Test Lifecycle (root-hooks.ts) + + **beforeAll** (runs once): + 1. Build the ndc-lambda-sdk (`npm run build` in `../ndc-lambda-sdk`) + 2. Create temp directory for DDN project + 3. `ddn supergraph init /test-project` + 4. `ddn connector init myjs --hub-connector hasura/nodejs --subgraph .../app/subgraph.yaml --configure-port 9876` + 5. Copy `fixtures/functions.ts` into connector directory + 6. Patch connector's `package.json` to use `file:` reference to local SDK + 7. `npm install` in connector directory + 8. Start connector via `node ndc-lambda-sdk/bin/index.js host -f functions.ts serve --configuration ./ --port 9876` + 9. Poll `/health` until 200 OK (30s timeout) + + **afterAll** (runs once): + 1. SIGTERM the connector process + 2. Remove temp directory + + ## Test Fixture (fixtures/functions.ts) + + Covers: + - **Scalar types**: string, number, boolean, bigint (all `@readonly`) + - **Optional/nullable args**: `string | null`, `value?: string` + - **Object types**: `Coordinates`, `Place` (nested objects as args and return) + - **Array types**: `string[]`, `number[]` args and returns + - **Nested return types**: `PersonWithAddress` with nested `address` object + - **Async functions**: `Promise`, `Promise` + - **Procedures** (no `@readonly`): `incrementCounter`, `resetCounter`, `createUser`, `asyncCreateItem` + - **SDK errors**: `sdk.Forbidden`, `sdk.Conflict`, `sdk.UnprocessableContent`, plain `Error` + + ## Files to Create + + 1. `integration-tests/package.json` - Dependencies: local SDK via `file:../ndc-lambda-sdk`, mocha, chai, ts-node, typescript + 2. `integration-tests/tsconfig.json` - Extends `@tsconfig/node20` + 3. `integration-tests/.mocharc.json` - 120s timeout, loads `root-hooks.ts` via `--require` + 4. `integration-tests/fixtures/functions.ts` - All test functions + 5. `integration-tests/src/helpers/temp-dir.ts` - `createTempDir()`, `removeTempDir()` + 6. `integration-tests/src/helpers/http-client.ts` - `createNdcClient()` returning typed client + 7. `integration-tests/src/helpers/connector-server.ts` - `startConnectorServer()`, health polling, `stop()` + 8. `integration-tests/src/helpers/ddn-project.ts` - `initSupergraph()`, `initConnector()`, `introspectConnector()`, `supergraphBuildLocal()` + 9. `integration-tests/src/tests/root-hooks.ts` - Mocha root hooks exporting shared `server`, `client`, `ddnProject` + 10. `integration-tests/src/tests/health.test.ts` - GET /health → 200 + 11. `integration-tests/src/tests/capabilities.test.ts` - GET /capabilities → query/mutation capabilities + 12. `integration-tests/src/tests/schema.test.ts` - Validates functions/procedures/scalars/objectTypes in schema + 13. `integration-tests/src/tests/query.test.ts` - ~15 test cases (hello, add, isTrue, bigint, nullable, optional, arrays, objects, nested, async, variables) + 14. `integration-tests/src/tests/mutation.test.ts` - Procedures: increment, create, async, state persistence + 15. `integration-tests/src/tests/error-handling.test.ts` - Error status codes (403, 409, 422, 500, 400) + 16. `integration-tests/src/tests/ddn-introspect.test.ts` - `ddn connector introspect` succeeds + 17. `integration-tests/src/tests/ddn-build.test.ts` - `ddn supergraph build local` succeeds, build artifacts exist + + ## Key Design Decisions + + - **Single server for all tests**: Server startup involves TS compilation (~5-15s). Shared instance via root hooks. + - **Port 9876**: Avoids conflicts with default 8080 and common dev ports. + - **Local SDK via `file:`**: Tests always run against repo code, not published version. + - **Node.js built-in `fetch`**: No extra HTTP client dependency (Node 22 has it). + - **`child_process.spawn`**: For connector process management (no cross-spawn needed in test code, Linux-only). + - **Temp directory in OS tmpdir**: Prevents DDN artifacts from polluting repo. + + ## Verification + + After implementation: + ```bash + cd ndc-lambda-sdk && npm ci && npm run build + cd ../integration-tests && npm install && npm test + ``` + + Expected: All tests pass (health, capabilities, schema, query, mutation, errors, DDN introspect, DDN build). diff --git a/integration-tests/fixtures/functions.ts b/integration-tests/fixtures/functions.ts new file mode 100644 index 0000000..995e614 --- /dev/null +++ b/integration-tests/fixtures/functions.ts @@ -0,0 +1,158 @@ +import * as sdk from "@hasura/ndc-lambda-sdk"; + +// ── Scalar types ── + +/** @readonly */ +export function hello(name?: string): string { + return `hello ${name ?? "world"}`; +} + +/** @readonly */ +export function add(a: number, b: number): number { + return a + b; +} + +/** @readonly */ +export function isTrue(value: boolean): boolean { + return value === true; +} + +/** @readonly */ +export function echoBigInt(value: bigint): bigint { + return value; +} + +// ── Nullable / optional args ── + +/** @readonly */ +export function greetNullable(name: string | null): string { + return `hello ${name ?? "anonymous"}`; +} + +/** @readonly */ +export function greetOptional(name?: string): string { + return `hello ${name ?? "default"}`; +} + +// ── Object types ── + +export type Coordinates = { + lat: number; + lng: number; +}; + +export type Place = { + name: string; + location: Coordinates; +}; + +/** @readonly */ +export function getDistance(from: Coordinates, to: Coordinates): number { + return Math.sqrt( + Math.pow(to.lat - from.lat, 2) + Math.pow(to.lng - from.lng, 2) + ); +} + +/** @readonly */ +export function describePlace(place: Place): string { + return `${place.name} is at (${place.location.lat}, ${place.location.lng})`; +} + +// ── Array types ── + +/** @readonly */ +export function sumArray(numbers: number[]): number { + return numbers.reduce((acc, n) => acc + n, 0); +} + +/** @readonly */ +export function reverseStrings(items: string[]): string[] { + return [...items].reverse(); +} + +// ── Nested return types ── + +type Address = { + street: string; + city: string; +}; + +type PersonWithAddress = { + name: string; + age: number; + address: Address; +}; + +/** @readonly */ +export function getPersonWithAddress(name: string, age: number, street: string, city: string): PersonWithAddress { + return { name, age, address: { street, city } }; +} + +// ── Async functions ── + +/** @readonly */ +export async function asyncGreet(name: string): Promise { + return `async hello ${name}`; +} + +/** @readonly */ +export async function asyncGetPlace(name: string, lat: number, lng: number): Promise { + return { name, location: { lat, lng } }; +} + +// ── Procedures (mutations) ── + +let counter = 0; + +export function incrementCounter(): number { + counter += 1; + return counter; +} + +export function resetCounter(): number { + counter = 0; + return counter; +} + +type User = { + id: number; + name: string; + email: string; +}; + +let nextUserId = 1; + +export function createUser(name: string, email: string): User { + return { id: nextUserId++, name, email }; +} + +type Item = { + id: string; + title: string; +}; + +export async function asyncCreateItem(title: string): Promise { + return { id: "item-1", title }; +} + +// ── SDK error functions ── + +/** @readonly */ +export function throwForbidden(): string { + throw new sdk.Forbidden("access denied", { reason: "no permission" }); +} + +/** @readonly */ +export function throwConflict(): string { + throw new sdk.Conflict("resource conflict", { resource: "item" }); +} + +/** @readonly */ +export function throwUnprocessable(): string { + throw new sdk.UnprocessableContent("invalid input", { field: "name" }); +} + +/** @readonly */ +export function throwInternalError(): string { + throw new Error("something went wrong"); +} diff --git a/integration-tests/package-lock.json b/integration-tests/package-lock.json new file mode 100644 index 0000000..09fef43 --- /dev/null +++ b/integration-tests/package-lock.json @@ -0,0 +1,1305 @@ +{ + "name": "ndc-nodejs-lambda-integration-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ndc-nodejs-lambda-integration-tests", + "version": "1.0.0", + "dependencies": { + "@hasura/ndc-lambda-sdk": "file:../ndc-lambda-sdk" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/chai": "^4.3.20", + "@types/mocha": "^10.0.10", + "@types/node": "^20.17.0", + "chai": "^4.5.0", + "mocha": "^10.8.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } + }, + "../ndc-lambda-sdk": { + "name": "@hasura/ndc-lambda-sdk", + "version": "1.20.2", + "license": "Apache-2.0", + "dependencies": { + "@hasura/ndc-sdk-typescript": "^8.4.0", + "@hasura/ts-node-dev": "^2.1.0", + "@tsconfig/node20": "^20.1.4", + "commander": "^11.1.0", + "cross-spawn": "^7.0.6", + "p-limit": "^3.1.0", + "ts-api-utils": "^2.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "bin": { + "ndc-lambda-sdk": "bin/index.js" + }, + "devDependencies": { + "@types/chai": "^4.3.11", + "@types/chai-as-promised": "^7.1.8", + "@types/mocha": "^10.0.6", + "chai": "^4.3.7", + "chai-as-promised": "^7.1.1", + "mocha": "^10.2.0", + "node-emoji": "^2.1.3", + "node-postgres": "^0.6.2" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@hasura/ndc-lambda-sdk": { + "resolved": "../ndc-lambda-sdk", + "link": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.9.tgz", + "integrity": "sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz", + "integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/integration-tests/package.json b/integration-tests/package.json new file mode 100644 index 0000000..bf1209f --- /dev/null +++ b/integration-tests/package.json @@ -0,0 +1,21 @@ +{ + "name": "ndc-nodejs-lambda-integration-tests", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "mocha" + }, + "dependencies": { + "@hasura/ndc-lambda-sdk": "file:../ndc-lambda-sdk" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/chai": "^4.3.20", + "@types/mocha": "^10.0.10", + "@types/node": "^20.17.0", + "chai": "^4.5.0", + "mocha": "^10.8.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} diff --git a/integration-tests/src/helpers/connector-server.ts b/integration-tests/src/helpers/connector-server.ts new file mode 100644 index 0000000..e0b9d14 --- /dev/null +++ b/integration-tests/src/helpers/connector-server.ts @@ -0,0 +1,77 @@ +import { spawn, ChildProcess } from "child_process"; +import * as path from "path"; + +export interface ConnectorServer { + process: ChildProcess; + port: number; + stop(): void; +} + +export function startConnectorServer(opts: { + functionsFile: string; + configurationDir: string; + port: number; +}): ConnectorServer { + const sdkBinPath = path.resolve(__dirname, "../../../ndc-lambda-sdk/bin/index.js"); + + const proc = spawn( + "node", + [ + sdkBinPath, + "host", + "-f", opts.functionsFile, + "serve", + "--configuration", opts.configurationDir, + "--port", String(opts.port), + ], + { + cwd: opts.configurationDir, + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, NODE_ENV: "test" }, + } + ); + + proc.stdout?.on("data", (data: Buffer) => { + if (process.env.DEBUG) { + process.stdout.write(`[connector stdout] ${data}`); + } + }); + + proc.stderr?.on("data", (data: Buffer) => { + if (process.env.DEBUG) { + process.stderr.write(`[connector stderr] ${data}`); + } + }); + + return { + process: proc, + port: opts.port, + stop() { + if (!proc.killed) { + proc.kill("SIGTERM"); + } + }, + }; +} + +export async function waitForHealth( + baseUrl: string, + timeoutMs: number = 30000 +): Promise { + const start = Date.now(); + const pollIntervalMs = 500; + + while (Date.now() - start < timeoutMs) { + try { + const response = await fetch(`${baseUrl}/health`); + if (response.ok) { + return; + } + } catch { + // Server not ready yet + } + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + } + + throw new Error(`Connector did not become healthy within ${timeoutMs}ms`); +} diff --git a/integration-tests/src/helpers/ddn-project.ts b/integration-tests/src/helpers/ddn-project.ts new file mode 100644 index 0000000..cb90e72 --- /dev/null +++ b/integration-tests/src/helpers/ddn-project.ts @@ -0,0 +1,107 @@ +import { execFileSync, ExecFileSyncOptionsWithStringEncoding } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; + +const execOpts: ExecFileSyncOptionsWithStringEncoding = { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 120000, +}; + +export function initSupergraph(targetDir: string): string { + execFileSync("ddn", ["supergraph", "init", targetDir], execOpts); + return targetDir; +} + +export function initConnector(opts: { + connectorName: string; + hubConnector: string; + subgraphPath: string; + configurePort: number; +}): string { + const output = execFileSync( + "ddn", + [ + "connector", "init", opts.connectorName, + "--hub-connector", opts.hubConnector, + "--subgraph", opts.subgraphPath, + "--configure-port", String(opts.configurePort), + ], + execOpts + ); + return output; +} + +export function introspectConnector(opts: { + connectorName: string; + subgraphPath: string; +}): string { + const output = execFileSync( + "ddn", + [ + "connector", "introspect", opts.connectorName, + "--subgraph", opts.subgraphPath, + ], + execOpts + ); + return output; +} + +export function supergraphBuildLocal(opts: { + supergraphPath: string; +}): string { + const output = execFileSync( + "ddn", + [ + "supergraph", "build", "local", + "--supergraph", opts.supergraphPath, + ], + execOpts + ); + return output; +} + +/** + * Manually update the DataConnectorLink HML file with schema from a running connector. + * This is an alternative to `ddn connector introspect` that doesn't require Docker. + */ +export async function updateDataConnectorLinkSchema(opts: { + connectorName: string; + ddnProjectDir: string; + connectorBaseUrl: string; +}): Promise { + // Fetch schema and capabilities from the running connector + const [schemaResponse, capabilitiesResponse] = await Promise.all([ + fetch(`${opts.connectorBaseUrl}/schema`), + fetch(`${opts.connectorBaseUrl}/capabilities`), + ]); + + if (!schemaResponse.ok) { + throw new Error(`Failed to fetch schema: ${schemaResponse.status}`); + } + if (!capabilitiesResponse.ok) { + throw new Error(`Failed to fetch capabilities: ${capabilitiesResponse.status}`); + } + + const schema = await schemaResponse.json(); + const capabilities = await capabilitiesResponse.json(); + + // Find and update the DataConnectorLink HML file + const hmlPath = path.join(opts.ddnProjectDir, "app", "metadata", `${opts.connectorName}.hml`); + const hmlContent = fs.readFileSync(hmlPath, "utf-8"); + + // Parse the HML (it's YAML-like but we'll do simple string replacement) + // The schema section looks like: + // schema: + // version: "" + // schema: {} + // capabilities: {} + + // We need to replace it with actual values + const updatedHml = hmlContent + .replace(/version: ""/g, 'version: "v0.2"') + .replace(/schema: \{\}/g, `schema: ${JSON.stringify(schema)}`) + .replace(/capabilities: \{\}/g, `capabilities: ${JSON.stringify(capabilities)}`); + + fs.writeFileSync(hmlPath, updatedHml); +} diff --git a/integration-tests/src/helpers/http-client.ts b/integration-tests/src/helpers/http-client.ts new file mode 100644 index 0000000..f6a8830 --- /dev/null +++ b/integration-tests/src/helpers/http-client.ts @@ -0,0 +1,33 @@ +export interface NdcClient { + baseUrl: string; + getHealth(): Promise; + getCapabilities(): Promise; + getSchema(): Promise; + postQuery(body: unknown): Promise; + postMutation(body: unknown): Promise; +} + +export function createNdcClient(port: number): NdcClient { + const baseUrl = `http://localhost:${port}`; + + async function get(path: string): Promise { + return fetch(`${baseUrl}${path}`); + } + + async function post(path: string, body: unknown): Promise { + return fetch(`${baseUrl}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + } + + return { + baseUrl, + getHealth: () => get("/health"), + getCapabilities: () => get("/capabilities"), + getSchema: () => get("/schema"), + postQuery: (body) => post("/query", body), + postMutation: (body) => post("/mutation", body), + }; +} diff --git a/integration-tests/src/helpers/temp-dir.ts b/integration-tests/src/helpers/temp-dir.ts new file mode 100644 index 0000000..d71258b --- /dev/null +++ b/integration-tests/src/helpers/temp-dir.ts @@ -0,0 +1,13 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +export function createTempDir(prefix: string = "ndc-lambda-integration-"): string { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +export function removeTempDir(dirPath: string): void { + if (fs.existsSync(dirPath)) { + fs.rmSync(dirPath, { recursive: true, force: true }); + } +} diff --git a/integration-tests/src/tests/capabilities.test.ts b/integration-tests/src/tests/capabilities.test.ts new file mode 100644 index 0000000..c79cf83 --- /dev/null +++ b/integration-tests/src/tests/capabilities.test.ts @@ -0,0 +1,15 @@ +import { expect } from "chai"; +import { client } from "./root-hooks"; + +describe("Capabilities endpoint", function () { + it("GET /capabilities returns query and mutation capabilities", async function () { + const response = await client.getCapabilities(); + expect(response.status).to.equal(200); + + const body: any = await response.json(); + expect(body).to.have.property("version"); + expect(body).to.have.property("capabilities"); + expect(body.capabilities).to.have.property("query"); + expect(body.capabilities).to.have.property("mutation"); + }); +}); diff --git a/integration-tests/src/tests/ddn-build.test.ts b/integration-tests/src/tests/ddn-build.test.ts new file mode 100644 index 0000000..75628df --- /dev/null +++ b/integration-tests/src/tests/ddn-build.test.ts @@ -0,0 +1,26 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as path from "path"; +import { ddnProjectDir } from "./root-hooks"; +import { supergraphBuildLocal } from "../helpers/ddn-project"; + +describe("DDN supergraph build", function () { + it("successfully builds the supergraph locally", function () { + this.timeout(60000); + + const supergraphPath = path.join(ddnProjectDir, "supergraph.yaml"); + + // Should not throw + const output = supergraphBuildLocal({ supergraphPath }); + expect(output).to.be.a("string"); + }); + + it("produces build artifacts", function () { + const engineBuildDir = path.join(ddnProjectDir, "engine", "build"); + expect(fs.existsSync(engineBuildDir)).to.be.true; + + // Check for at least one build output file + const files = fs.readdirSync(engineBuildDir); + expect(files.length).to.be.greaterThan(0); + }); +}); diff --git a/integration-tests/src/tests/ddn-introspect.test.ts b/integration-tests/src/tests/ddn-introspect.test.ts new file mode 100644 index 0000000..013e24f --- /dev/null +++ b/integration-tests/src/tests/ddn-introspect.test.ts @@ -0,0 +1,26 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as path from "path"; +import { ddnProjectDir } from "./root-hooks"; + +describe("DDN connector introspect", function () { + it("DataConnectorLink has valid schema after manual introspection", function () { + // The root-hooks.ts calls updateDataConnectorLinkSchema() which fetches + // the schema from the running connector and updates the HML file. + // This test verifies that the schema was properly updated. + + const hmlPath = path.join(ddnProjectDir, "app", "metadata", "myjs.hml"); + const hmlContent = fs.readFileSync(hmlPath, "utf-8"); + + // Verify the version was updated from empty string + expect(hmlContent).to.include('version: "v0.2"'); + + // Verify the schema contains actual function definitions + expect(hmlContent).to.include('"functions"'); + expect(hmlContent).to.include('"procedures"'); + + // Verify capabilities were populated + expect(hmlContent).to.include('"query"'); + expect(hmlContent).to.include('"mutation"'); + }); +}); diff --git a/integration-tests/src/tests/error-handling.test.ts b/integration-tests/src/tests/error-handling.test.ts new file mode 100644 index 0000000..0842492 --- /dev/null +++ b/integration-tests/src/tests/error-handling.test.ts @@ -0,0 +1,57 @@ +import { expect } from "chai"; +import { client } from "./root-hooks"; + +function makeQueryRequest(collection: string): object { + return { + collection, + query: { + fields: { + __value: { type: "column", column: "__value" }, + }, + }, + arguments: {}, + collection_relationships: {}, + }; +} + +describe("Error handling", function () { + it("throwForbidden returns 403", async function () { + const response = await client.postQuery(makeQueryRequest("throwForbidden")); + expect(response.status).to.equal(403); + const body: any = await response.json(); + expect(body.message).to.include("access denied"); + }); + + it("throwConflict returns 409", async function () { + const response = await client.postQuery(makeQueryRequest("throwConflict")); + expect(response.status).to.equal(409); + const body: any = await response.json(); + expect(body.message).to.include("resource conflict"); + }); + + it("throwUnprocessable returns 422", async function () { + const response = await client.postQuery(makeQueryRequest("throwUnprocessable")); + expect(response.status).to.equal(422); + const body: any = await response.json(); + expect(body.message).to.include("invalid input"); + }); + + it("throwInternalError returns 500", async function () { + const response = await client.postQuery(makeQueryRequest("throwInternalError")); + expect(response.status).to.equal(500); + const body: any = await response.json(); + expect(body.message).to.be.a("string"); + }); + + it("querying a nonexistent function returns 400", async function () { + const response = await client.postQuery(makeQueryRequest("nonexistentFunction")); + expect(response.status).to.be.oneOf([400, 500]); + }); + + it("calling a procedure via query endpoint returns 400", async function () { + const response = await client.postQuery(makeQueryRequest("incrementCounter")); + expect(response.status).to.equal(400); + const body: any = await response.json(); + expect(body.message).to.be.a("string"); + }); +}); diff --git a/integration-tests/src/tests/health.test.ts b/integration-tests/src/tests/health.test.ts new file mode 100644 index 0000000..e336707 --- /dev/null +++ b/integration-tests/src/tests/health.test.ts @@ -0,0 +1,9 @@ +import { expect } from "chai"; +import { client } from "./root-hooks"; + +describe("Health endpoint", function () { + it("GET /health returns 200", async function () { + const response = await client.getHealth(); + expect(response.status).to.equal(200); + }); +}); diff --git a/integration-tests/src/tests/mutation.test.ts b/integration-tests/src/tests/mutation.test.ts new file mode 100644 index 0000000..c05b2ff --- /dev/null +++ b/integration-tests/src/tests/mutation.test.ts @@ -0,0 +1,122 @@ +import { expect } from "chai"; +import { client } from "./root-hooks"; + +function makeMutationRequest( + procedureName: string, + args: Record, + fields?: object | null +): object { + return { + operations: [ + { + type: "procedure", + name: procedureName, + arguments: args, + fields: fields ?? null, + }, + ], + collection_relationships: {}, + }; +} + +async function mutateScalar(procedureName: string, args: Record): Promise { + const response = await client.postMutation(makeMutationRequest(procedureName, args)); + expect(response.status).to.equal(200); + const body: any = await response.json(); + return body.operation_results?.[0]?.result; +} + +describe("Mutation tests", function () { + describe("counter procedures", function () { + it("resetCounter resets to zero", async function () { + const result = await mutateScalar("resetCounter", {}); + expect(result).to.equal(0); + }); + + it("incrementCounter increments the counter", async function () { + // Reset first + await mutateScalar("resetCounter", {}); + + const first = await mutateScalar("incrementCounter", {}); + expect(first).to.equal(1); + + const second = await mutateScalar("incrementCounter", {}); + expect(second).to.equal(2); + }); + + it("counter state persists across calls", async function () { + await mutateScalar("resetCounter", {}); + await mutateScalar("incrementCounter", {}); + await mutateScalar("incrementCounter", {}); + await mutateScalar("incrementCounter", {}); + + const result = await mutateScalar("incrementCounter", {}); + expect(result).to.equal(4); + }); + }); + + describe("createUser procedure", function () { + it("creates a user with name and email", async function () { + const response = await client.postMutation( + makeMutationRequest( + "createUser", + { name: "Alice", email: "alice@example.com" }, + { + type: "object", + fields: { + name: { type: "column", column: "name" }, + email: { type: "column", column: "email" }, + }, + } + ) + ); + expect(response.status).to.equal(200); + const body: any = await response.json(); + const result = body.operation_results?.[0]?.result; + expect(result).to.deep.include({ name: "Alice", email: "alice@example.com" }); + }); + + it("returns user with id field", async function () { + const response = await client.postMutation( + makeMutationRequest( + "createUser", + { name: "Bob", email: "bob@example.com" }, + { + type: "object", + fields: { + id: { type: "column", column: "id" }, + name: { type: "column", column: "name" }, + }, + } + ) + ); + expect(response.status).to.equal(200); + const body: any = await response.json(); + const result = body.operation_results?.[0]?.result; + expect(result.id).to.be.a("number"); + expect(result.name).to.equal("Bob"); + }); + }); + + describe("async procedure", function () { + it("asyncCreateItem creates an item asynchronously", async function () { + const response = await client.postMutation( + makeMutationRequest( + "asyncCreateItem", + { title: "Test Item" }, + { + type: "object", + fields: { + id: { type: "column", column: "id" }, + title: { type: "column", column: "title" }, + }, + } + ) + ); + expect(response.status).to.equal(200); + const body: any = await response.json(); + const result = body.operation_results?.[0]?.result; + expect(result).to.deep.equal({ id: "item-1", title: "Test Item" }); + }); + }); +}); diff --git a/integration-tests/src/tests/query.test.ts b/integration-tests/src/tests/query.test.ts new file mode 100644 index 0000000..ee8ed11 --- /dev/null +++ b/integration-tests/src/tests/query.test.ts @@ -0,0 +1,277 @@ +import { expect } from "chai"; +import { client } from "./root-hooks"; + +function makeQueryRequest( + collection: string, + args: Record, + fields?: Record, + variables?: Record[] +): object { + const request: any = { + collection, + query: { + fields: fields ?? { + __value: { type: "column", column: "__value" }, + }, + }, + arguments: Object.fromEntries( + Object.entries(args).map(([k, v]) => [k, { type: "literal", value: v }]) + ), + collection_relationships: {}, + }; + if (variables) { + request.variables = variables; + } + return request; +} + +function makeVariableQueryRequest( + collection: string, + literalArgs: Record, + variableArgs: Record, + variables: Record[] +): object { + const arguments_: Record = {}; + for (const [k, v] of Object.entries(literalArgs)) { + arguments_[k] = { type: "literal", value: v }; + } + for (const [k, v] of Object.entries(variableArgs)) { + arguments_[k] = { type: "variable", name: v }; + } + return { + collection, + query: { + fields: { + __value: { type: "column", column: "__value" }, + }, + }, + arguments: arguments_, + collection_relationships: {}, + variables, + }; +} + +async function queryScalar(collection: string, args: Record): Promise { + const response = await client.postQuery(makeQueryRequest(collection, args)); + expect(response.status).to.equal(200); + const body: any = await response.json(); + return body[0]?.rows?.[0]?.__value; +} + +describe("Query tests", function () { + describe("scalar types", function () { + it("hello with no args returns default greeting", async function () { + const result = await queryScalar("hello", {}); + expect(result).to.equal("hello world"); + }); + + it("hello with name arg returns personalized greeting", async function () { + const result = await queryScalar("hello", { name: "Alice" }); + expect(result).to.equal("hello Alice"); + }); + + it("add returns sum of two numbers", async function () { + const result = await queryScalar("add", { a: 3, b: 7 }); + expect(result).to.equal(10); + }); + + it("isTrue returns boolean correctly", async function () { + const trueResult = await queryScalar("isTrue", { value: true }); + expect(trueResult).to.equal(true); + + const falseResult = await queryScalar("isTrue", { value: false }); + expect(falseResult).to.equal(false); + }); + + it("echoBigInt echoes back a bigint value", async function () { + const result = await queryScalar("echoBigInt", { value: "12345678901234567890" }); + expect(result).to.equal("12345678901234567890"); + }); + }); + + describe("nullable and optional arguments", function () { + it("greetNullable with null returns anonymous", async function () { + const result = await queryScalar("greetNullable", { name: null }); + expect(result).to.equal("hello anonymous"); + }); + + it("greetNullable with a value returns greeting", async function () { + const result = await queryScalar("greetNullable", { name: "Bob" }); + expect(result).to.equal("hello Bob"); + }); + + it("greetOptional with no args returns default", async function () { + const result = await queryScalar("greetOptional", {}); + expect(result).to.equal("hello default"); + }); + + it("greetOptional with a value returns greeting", async function () { + const result = await queryScalar("greetOptional", { name: "Charlie" }); + expect(result).to.equal("hello Charlie"); + }); + }); + + describe("array types", function () { + it("sumArray returns sum of number array", async function () { + const result = await queryScalar("sumArray", { numbers: [1, 2, 3, 4, 5] }); + expect(result).to.equal(15); + }); + + it("reverseStrings returns reversed array", async function () { + const result = await queryScalar("reverseStrings", { items: ["a", "b", "c"] }); + expect(result).to.deep.equal(["c", "b", "a"]); + }); + }); + + describe("object types", function () { + it("getDistance computes distance between coordinates", async function () { + const result = await queryScalar("getDistance", { + from: { lat: 0, lng: 0 }, + to: { lat: 3, lng: 4 }, + }); + expect(result).to.equal(5); + }); + + it("describePlace returns a description string", async function () { + const result = await queryScalar("describePlace", { + place: { name: "Office", location: { lat: 40.7, lng: -74.0 } }, + }); + expect(result).to.equal("Office is at (40.7, -74)"); + }); + }); + + describe("nested return types", function () { + it("getPersonWithAddress returns nested object with field selection", async function () { + const response = await client.postQuery({ + collection: "getPersonWithAddress", + query: { + fields: { + __value: { + type: "column", + column: "__value", + fields: { + type: "object", + fields: { + name: { type: "column", column: "name" }, + age: { type: "column", column: "age" }, + address: { + type: "column", + column: "address", + fields: { + type: "object", + fields: { + street: { type: "column", column: "street" }, + city: { type: "column", column: "city" }, + }, + }, + }, + }, + }, + }, + }, + }, + arguments: { + name: { type: "literal", value: "Alice" }, + age: { type: "literal", value: 30 }, + street: { type: "literal", value: "123 Main St" }, + city: { type: "literal", value: "Springfield" }, + }, + collection_relationships: {}, + }); + expect(response.status).to.equal(200); + const body: any = await response.json(); + const value = body[0]?.rows?.[0]?.__value; + expect(value).to.deep.equal({ + name: "Alice", + age: 30, + address: { + street: "123 Main St", + city: "Springfield", + }, + }); + }); + }); + + describe("async functions", function () { + it("asyncGreet returns async greeting", async function () { + const result = await queryScalar("asyncGreet", { name: "Dave" }); + expect(result).to.equal("async hello Dave"); + }); + + it("asyncGetPlace returns async place object", async function () { + const response = await client.postQuery({ + collection: "asyncGetPlace", + query: { + fields: { + __value: { + type: "column", + column: "__value", + fields: { + type: "object", + fields: { + name: { type: "column", column: "name" }, + location: { + type: "column", + column: "location", + fields: { + type: "object", + fields: { + lat: { type: "column", column: "lat" }, + lng: { type: "column", column: "lng" }, + }, + }, + }, + }, + }, + }, + }, + }, + arguments: { + name: { type: "literal", value: "Park" }, + lat: { type: "literal", value: 51.5 }, + lng: { type: "literal", value: -0.1 }, + }, + collection_relationships: {}, + }); + expect(response.status).to.equal(200); + const body: any = await response.json(); + const value = body[0]?.rows?.[0]?.__value; + expect(value).to.deep.equal({ + name: "Park", + location: { lat: 51.5, lng: -0.1 }, + }); + }); + }); + + describe("variables", function () { + it("executes query with variable arguments for each variable set", async function () { + const request = makeVariableQueryRequest( + "hello", + {}, + { name: "nameVar" }, + [{ nameVar: "Var1" }, { nameVar: "Var2" }] + ); + const response = await client.postQuery(request); + expect(response.status).to.equal(200); + const body: any = await response.json(); + expect(body).to.have.lengthOf(2); + expect(body[0]?.rows?.[0]?.__value).to.equal("hello Var1"); + expect(body[1]?.rows?.[0]?.__value).to.equal("hello Var2"); + }); + + it("mixes literal and variable arguments", async function () { + const request = makeVariableQueryRequest( + "add", + { a: 10 }, + { b: "bVar" }, + [{ bVar: 5 }, { bVar: 20 }] + ); + const response = await client.postQuery(request); + expect(response.status).to.equal(200); + const body: any = await response.json(); + expect(body).to.have.lengthOf(2); + expect(body[0]?.rows?.[0]?.__value).to.equal(15); + expect(body[1]?.rows?.[0]?.__value).to.equal(30); + }); + }); +}); diff --git a/integration-tests/src/tests/root-hooks.ts b/integration-tests/src/tests/root-hooks.ts new file mode 100644 index 0000000..ee94e7e --- /dev/null +++ b/integration-tests/src/tests/root-hooks.ts @@ -0,0 +1,115 @@ +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { createTempDir, removeTempDir } from "../helpers/temp-dir"; +import { createNdcClient, NdcClient } from "../helpers/http-client"; +import { + startConnectorServer, + waitForHealth, + ConnectorServer, +} from "../helpers/connector-server"; +import { + initSupergraph, + initConnector, + updateDataConnectorLinkSchema, +} from "../helpers/ddn-project"; + +const CONNECTOR_PORT = 9876; +const CONNECTOR_NAME = "myjs"; + +export let server: ConnectorServer; +export let client: NdcClient; +export let ddnProjectDir: string; +export let connectorDir: string; + +let tempDir: string; + +export const mochaHooks = { + async beforeAll(this: Mocha.Context) { + this.timeout(180000); + + // 1. Build the ndc-lambda-sdk + const sdkDir = path.resolve(__dirname, "../../../ndc-lambda-sdk"); + console.log(" Building ndc-lambda-sdk..."); + execSync("npm ci && npm run build", { cwd: sdkDir, stdio: "pipe" }); + + // 2. Create temp directory and init DDN project + tempDir = createTempDir(); + ddnProjectDir = path.join(tempDir, "test-project"); + console.log(` Initializing DDN project in ${ddnProjectDir}...`); + initSupergraph(ddnProjectDir); + + // 3. Init connector + console.log(" Initializing connector..."); + const subgraphPath = path.join(ddnProjectDir, "app", "subgraph.yaml"); + process.chdir(ddnProjectDir); + initConnector({ + connectorName: CONNECTOR_NAME, + hubConnector: "hasura/nodejs", + subgraphPath: subgraphPath, + configurePort: CONNECTOR_PORT, + }); + + // 4. Set up connector directory + connectorDir = path.join(ddnProjectDir, "app", "connector", CONNECTOR_NAME); + + // 5. Copy test fixtures functions.ts into connector directory + const fixturesSource = path.resolve(__dirname, "../../fixtures/functions.ts"); + const functionsDest = path.join(connectorDir, "functions.ts"); + fs.copyFileSync(fixturesSource, functionsDest); + + // 6. Patch connector's package.json to use local SDK + // Also need to add @tsconfig/node20 explicitly because it's referenced by the connector's + // tsconfig.json but isn't hoisted when using file: references to the local SDK + const pkgPath = path.join(connectorDir, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + const absoluteSdkPath = path.resolve(__dirname, "../../../ndc-lambda-sdk"); + pkg.dependencies["@hasura/ndc-lambda-sdk"] = `file:${absoluteSdkPath}`; + pkg.dependencies["@tsconfig/node20"] = "^20.1.4"; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); + + // 7. npm install in connector directory + console.log(" Installing connector dependencies..."); + execSync("npm install", { cwd: connectorDir, stdio: "pipe" }); + + // 8. Start connector server + console.log(" Starting connector server..."); + server = startConnectorServer({ + functionsFile: "functions.ts", + configurationDir: connectorDir, + port: CONNECTOR_PORT, + }); + + // 9. Wait for health + client = createNdcClient(CONNECTOR_PORT); + console.log(" Waiting for connector to be healthy..."); + await waitForHealth(client.baseUrl, 60000); + console.log(" Connector is healthy."); + + // 10. Update DataConnectorLink with schema from running connector + // This is needed for DDN supergraph build to work without Docker + console.log(" Updating DataConnectorLink schema..."); + await updateDataConnectorLinkSchema({ + connectorName: CONNECTOR_NAME, + ddnProjectDir, + connectorBaseUrl: client.baseUrl, + }); + console.log(" DataConnectorLink schema updated."); + }, + + async afterAll(this: Mocha.Context) { + this.timeout(30000); + + if (server) { + console.log(" Stopping connector server..."); + server.stop(); + // Wait briefly for process to terminate + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + if (tempDir) { + console.log(" Cleaning up temp directory..."); + removeTempDir(tempDir); + } + }, +}; diff --git a/integration-tests/src/tests/schema.test.ts b/integration-tests/src/tests/schema.test.ts new file mode 100644 index 0000000..fc010d5 --- /dev/null +++ b/integration-tests/src/tests/schema.test.ts @@ -0,0 +1,125 @@ +import { expect } from "chai"; +import { client } from "./root-hooks"; + +describe("Schema endpoint", function () { + let schema: any; + + before(async function () { + const response = await client.getSchema(); + expect(response.status).to.equal(200); + schema = await response.json(); + }); + + it("returns functions and procedures", function () { + expect(schema).to.have.property("functions").that.is.an("array"); + expect(schema).to.have.property("procedures").that.is.an("array"); + }); + + it("contains scalar type definitions", function () { + expect(schema).to.have.property("scalar_types"); + const scalarNames = Object.keys(schema.scalar_types); + expect(scalarNames).to.include("String"); + expect(scalarNames).to.include("Float"); + expect(scalarNames).to.include("Boolean"); + expect(scalarNames).to.include("BigInt"); + }); + + it("contains object type definitions", function () { + expect(schema).to.have.property("object_types"); + const objectNames = Object.keys(schema.object_types); + expect(objectNames).to.include("Coordinates"); + expect(objectNames).to.include("Place"); + }); + + describe("functions (queries)", function () { + it("includes hello function", function () { + const fn = schema.functions.find((f: any) => f.name === "hello"); + expect(fn).to.exist; + expect(fn.result_type).to.deep.include({ type: "named", name: "String" }); + }); + + it("includes add function", function () { + const fn = schema.functions.find((f: any) => f.name === "add"); + expect(fn).to.exist; + // arguments is an object, not an array + expect(Object.keys(fn.arguments)).to.have.lengthOf(2); + expect(fn.arguments).to.have.property("a"); + expect(fn.arguments).to.have.property("b"); + }); + + it("includes isTrue function", function () { + const fn = schema.functions.find((f: any) => f.name === "isTrue"); + expect(fn).to.exist; + }); + + it("includes echoBigInt function", function () { + const fn = schema.functions.find((f: any) => f.name === "echoBigInt"); + expect(fn).to.exist; + }); + + it("includes nullable and optional argument functions", function () { + const greetNullable = schema.functions.find((f: any) => f.name === "greetNullable"); + expect(greetNullable).to.exist; + + const greetOptional = schema.functions.find((f: any) => f.name === "greetOptional"); + expect(greetOptional).to.exist; + }); + + it("includes object-arg functions", function () { + const getDistance = schema.functions.find((f: any) => f.name === "getDistance"); + expect(getDistance).to.exist; + + const describePlace = schema.functions.find((f: any) => f.name === "describePlace"); + expect(describePlace).to.exist; + }); + + it("includes array functions", function () { + const sumArray = schema.functions.find((f: any) => f.name === "sumArray"); + expect(sumArray).to.exist; + + const reverseStrings = schema.functions.find((f: any) => f.name === "reverseStrings"); + expect(reverseStrings).to.exist; + }); + + it("includes async functions", function () { + const asyncGreet = schema.functions.find((f: any) => f.name === "asyncGreet"); + expect(asyncGreet).to.exist; + + const asyncGetPlace = schema.functions.find((f: any) => f.name === "asyncGetPlace"); + expect(asyncGetPlace).to.exist; + }); + + it("includes error-throwing functions", function () { + expect(schema.functions.find((f: any) => f.name === "throwForbidden")).to.exist; + expect(schema.functions.find((f: any) => f.name === "throwConflict")).to.exist; + expect(schema.functions.find((f: any) => f.name === "throwUnprocessable")).to.exist; + expect(schema.functions.find((f: any) => f.name === "throwInternalError")).to.exist; + }); + }); + + describe("procedures (mutations)", function () { + it("includes incrementCounter", function () { + const proc = schema.procedures.find((p: any) => p.name === "incrementCounter"); + expect(proc).to.exist; + }); + + it("includes resetCounter", function () { + const proc = schema.procedures.find((p: any) => p.name === "resetCounter"); + expect(proc).to.exist; + }); + + it("includes createUser", function () { + const proc = schema.procedures.find((p: any) => p.name === "createUser"); + expect(proc).to.exist; + // arguments is an object, not an array + expect(Object.keys(proc.arguments)).to.have.lengthOf(2); + expect(proc.arguments).to.have.property("name"); + expect(proc.arguments).to.have.property("email"); + }); + + it("includes asyncCreateItem", function () { + const proc = schema.procedures.find((p: any) => p.name === "asyncCreateItem"); + expect(proc).to.exist; + }); + }); +}); diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json new file mode 100644 index 0000000..15a2722 --- /dev/null +++ b/integration-tests/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "sourceMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"] +}