From 92150090393f4a611c186aad860f878199e81f0a Mon Sep 17 00:00:00 2001 From: CodeMash Date: Mon, 12 Oct 2026 14:20:03 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(code):=20hub.code=20hooks=20=E2=80=94?= =?UTF-8?q?=20integrations=20and=20cloud=20functions=20on=20@norbix.ai/ts?= =?UTF-8?q?=204.19.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/hooks/hub/code.ts wraps the Code module switch, the eight Code integration endpoints and the five cloud-function endpoints the core SDK gained in 4.19.0 (getCodeFunctions, getCodeFunctionQualifiers, invokeCodeFunction, getCodeFunctionEventSample, getCodeFunctionRuns): queries provide CodeIntegrations/LIST| and CodeFunctions/LIST|RUNS|SAMPLE, mutations invalidate the list and the integration; the marketplace half of hub.code is not wrapped. README section with the hook table. The bump to 4.19.0 also drops the legacy X-CM-ProjectId header (sdk-ts 4.18.1): four route tests now assert norbix-project-id. Co-Authored-By: Claude Fable 5.1 --- README.md | 104 ++++++++++------ package-lock.json | 8 +- package.json | 2 +- src/hooks/hub/code.ts | 135 +++++++++++++++++++++ src/hooks/index.ts | 2 + tests/code.test.ts | 256 ++++++++++++++++++++++++++++++++++++++++ tests/database.test.ts | 2 +- tests/files.test.ts | 2 +- tests/scheduler.test.ts | 2 +- tests/sms.test.ts | 2 +- 10 files changed, 473 insertions(+), 42 deletions(-) create mode 100644 src/hooks/hub/code.ts create mode 100644 tests/code.test.ts diff --git a/README.md b/README.md index 7e23f90..46768ee 100644 --- a/README.md +++ b/README.md @@ -187,40 +187,40 @@ A _curated_ set of hooks for the most common endpoints. Every hook is end-to-end Every `hub.database` method (73) and every `api.database` method (22) of `@norbix.ai/ts` has a hook; Hub hooks keep the SDK method name. The rows below are the ones added for `@norbix.ai/ts` >= 4.9.0. -| Hook | SDK method | Cache | -| --------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------ | -| `useFindRecordsQuery` | `hub.database.findRecords` | provides `DatabaseRecords/` | -| `useFindOneRecordQuery` | `hub.database.findOneRecord` | provides `DatabaseRecords/` | -| `useCountRecordsQuery` | `hub.database.countRecords` | provides `DatabaseRecords/` | -| `useDistinctRecordValuesQuery` | `hub.database.distinctRecordValues` | provides `DatabaseRecords/` | -| `useAggregateRecordsMutation` | `hub.database.aggregateRecords` | — (never cached) | -| `useExecuteRecordsAggregateMutation` | `hub.database.executeRecordsAggregate` | — (never cached) | -| `useGetCollectionIndexesQuery` | `hub.database.getCollectionIndexes` | provides `DatabaseCollections/` | -| `useInsertRecordMutation` | `hub.database.insertRecord` | invalidates `DatabaseRecords/` | -| `useInsertManyRecordsMutation` | `hub.database.insertManyRecords` | invalidates `DatabaseRecords/` | -| `useUpdateOneRecordMutation` | `hub.database.updateOneRecord` | invalidates `DatabaseRecords/` | -| `useUpdateManyRecordsMutation` | `hub.database.updateManyRecords` | invalidates `DatabaseRecords/` | -| `useReplaceRecordMutation` | `hub.database.replaceRecord` | invalidates `DatabaseRecords/` | -| `useDeleteRecordMutation` | `hub.database.deleteRecord` | invalidates `DatabaseRecords/` | -| `useDeleteManyRecordsMutation` | `hub.database.deleteManyRecords` | invalidates `DatabaseRecords/` | -| `useChangeRecordResponsibilityMutation` | `hub.database.changeRecordResponsibility` | invalidates `DatabaseRecords/` | -| `useSeedCollectionRecordsMutation` | `hub.database.seedCollectionRecords` | invalidates `DatabaseRecords` | -| `useGetDatabaseTaxonomyTreeQuery` | `hub.database.getDatabaseTaxonomyTree` | provides `DatabaseTaxonomies` + `DatabaseTaxonomyTerms` | -| `useGetDatabaseTaxonomyTermTreeQuery` | `hub.database.getDatabaseTaxonomyTermTree` | provides `DatabaseTaxonomyTerms/` | -| `useGetDatabaseMergedTermTreeQuery` | `hub.database.getDatabaseMergedTermTree` | provides `DatabaseTaxonomyTerms/` | -| `useApplyDatabaseSchemaBundleMutation` | `hub.database.applyDatabaseSchemaBundle` | invalidates `DatabaseSchemas` + taxonomies + terms | -| `useUpdateDatabaseSchemaEmbedMutation` | `hub.database.updateDatabaseSchemaEmbed` | invalidates `DatabaseSchemas` | -| `useGetDatabaseSchemaListSettingsQuery` | `hub.database.getDatabaseSchemaListSettings` | provides `DatabaseSchemas` | +| Hook | SDK method | Cache | +| --------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `useFindRecordsQuery` | `hub.database.findRecords` | provides `DatabaseRecords/` | +| `useFindOneRecordQuery` | `hub.database.findOneRecord` | provides `DatabaseRecords/` | +| `useCountRecordsQuery` | `hub.database.countRecords` | provides `DatabaseRecords/` | +| `useDistinctRecordValuesQuery` | `hub.database.distinctRecordValues` | provides `DatabaseRecords/` | +| `useAggregateRecordsMutation` | `hub.database.aggregateRecords` | — (never cached) | +| `useExecuteRecordsAggregateMutation` | `hub.database.executeRecordsAggregate` | — (never cached) | +| `useGetCollectionIndexesQuery` | `hub.database.getCollectionIndexes` | provides `DatabaseCollections/` | +| `useInsertRecordMutation` | `hub.database.insertRecord` | invalidates `DatabaseRecords/` | +| `useInsertManyRecordsMutation` | `hub.database.insertManyRecords` | invalidates `DatabaseRecords/` | +| `useUpdateOneRecordMutation` | `hub.database.updateOneRecord` | invalidates `DatabaseRecords/` | +| `useUpdateManyRecordsMutation` | `hub.database.updateManyRecords` | invalidates `DatabaseRecords/` | +| `useReplaceRecordMutation` | `hub.database.replaceRecord` | invalidates `DatabaseRecords/` | +| `useDeleteRecordMutation` | `hub.database.deleteRecord` | invalidates `DatabaseRecords/` | +| `useDeleteManyRecordsMutation` | `hub.database.deleteManyRecords` | invalidates `DatabaseRecords/` | +| `useChangeRecordResponsibilityMutation` | `hub.database.changeRecordResponsibility` | invalidates `DatabaseRecords/` | +| `useSeedCollectionRecordsMutation` | `hub.database.seedCollectionRecords` | invalidates `DatabaseRecords` | +| `useGetDatabaseTaxonomyTreeQuery` | `hub.database.getDatabaseTaxonomyTree` | provides `DatabaseTaxonomies` + `DatabaseTaxonomyTerms` | +| `useGetDatabaseTaxonomyTermTreeQuery` | `hub.database.getDatabaseTaxonomyTermTree` | provides `DatabaseTaxonomyTerms/` | +| `useGetDatabaseMergedTermTreeQuery` | `hub.database.getDatabaseMergedTermTree` | provides `DatabaseTaxonomyTerms/` | +| `useApplyDatabaseSchemaBundleMutation` | `hub.database.applyDatabaseSchemaBundle` | invalidates `DatabaseSchemas` + taxonomies + terms | +| `useUpdateDatabaseSchemaEmbedMutation` | `hub.database.updateDatabaseSchemaEmbed` | invalidates `DatabaseSchemas` | +| `useGetDatabaseSchemaListSettingsQuery` | `hub.database.getDatabaseSchemaListSettings` | provides `DatabaseSchemas` | | `useGetDatabaseSchemaIndexStatusQuery` | `hub.database.getDatabaseSchemaIndexStatus` | provides `DatabaseSchemas/` — the last schema-index run (building / ready / refused / partial) | -| `useUpdateDatabaseSchemaListSettingsMutation` | `hub.database.updateDatabaseSchemaListSettings` | invalidates `DatabaseSchemas` | -| `useGetCollectionImportsQuery` | `hub.database.getCollectionImports` | provides `DatabaseImports` | -| `useGetCollectionImportQuery` | `hub.database.getCollectionImport` | provides `DatabaseImports` | -| `useCreateCollectionImportMutation` | `hub.database.createCollectionImport` | invalidates `DatabaseImports` + `DatabaseRecords/` | -| `useDeleteCollectionImportMutation` | `hub.database.deleteCollectionImport` | invalidates `DatabaseImports` | -| `useAnalyzeImportFileMutation` | `hub.database.analyzeImportFile` | — | -| `useRequestImportUploadUrlMutation` | `hub.database.requestImportUploadUrl` | — | -| `useFindOwnQuery` | `api.database.findOwn` | provides `DatabaseCollections/` | -| `useFindMergedTermTreeQuery` | `api.database.findMergedTermTree` | provides `DatabaseTaxonomyTerms/` | +| `useUpdateDatabaseSchemaListSettingsMutation` | `hub.database.updateDatabaseSchemaListSettings` | invalidates `DatabaseSchemas` | +| `useGetCollectionImportsQuery` | `hub.database.getCollectionImports` | provides `DatabaseImports` | +| `useGetCollectionImportQuery` | `hub.database.getCollectionImport` | provides `DatabaseImports` | +| `useCreateCollectionImportMutation` | `hub.database.createCollectionImport` | invalidates `DatabaseImports` + `DatabaseRecords/` | +| `useDeleteCollectionImportMutation` | `hub.database.deleteCollectionImport` | invalidates `DatabaseImports` | +| `useAnalyzeImportFileMutation` | `hub.database.analyzeImportFile` | — | +| `useRequestImportUploadUrlMutation` | `hub.database.requestImportUploadUrl` | — | +| `useFindOwnQuery` | `api.database.findOwn` | provides `DatabaseCollections/` | +| `useFindMergedTermTreeQuery` | `api.database.findMergedTermTree` | provides `DatabaseTaxonomyTerms/` | ### Module switches send `PUT` (core SDK 4.10.0) @@ -506,6 +506,44 @@ The typed `task` body (plain `'EmailCampaign'` / `'AllUsers'` strings, no cast) comes from `@norbix.ai/ts` 4.6.0. Full method reference: the core SDK's `docs/hub/scheduler.md`. +## Code — integrations and cloud functions + +Every `hub.code` endpoint outside the marketplace (15) has a hook, 1:1 with +`norbix.hub.code`: the module switch, the Code integrations (AWS Lambda, Azure +Functions, Google Cloud Functions credentials) and the cloud functions they +may call — list, aliases / versions, a call by hand, a sample envelope for a +local run, and the last runs of a trigger or task. The five function hooks +need `@norbix.ai/ts` 4.19.0. + +| Area | Hooks | Cache tag | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Module switch | `useEnableCodeModuleMutation`, `useDisableCodeModuleMutation` | invalidates `Account`, `Projects`, `Code` | +| Integrations | `useGetCodeIntegrationsQuery`, `useGetCodeIntegrationQuery`, `useSaveCodeIntegrationMutation`, `useDeleteCodeIntegrationMutation`, `useEnableCodeIntegrationMutation`, `useDisableCodeIntegrationMutation`, `useSetCodeIntegrationAsDefaultMutation`, `useTestCodeIntegrationMutation` | provides / invalidates `CodeIntegrations/LIST` and `CodeIntegrations/`; `test` touches nothing | +| Cloud functions | `useGetCodeFunctionsQuery`, `useGetCodeFunctionQualifiersQuery`, `useGetCodeFunctionEventSampleQuery`, `useGetCodeFunctionRunsQuery`, `useInvokeCodeFunctionMutation` | provides `CodeFunctions/LIST`, `CodeFunctions/SAMPLE`, `CodeFunctions/RUNS`; `invoke` touches nothing | + +```tsx +const integrationId = 'int_6rBACnEaxQ0x1jTiHtAuWV'; +const { data } = useGetCodeFunctionsQuery({ integrationId, prefix: 'orders-' }); +const [invoke, { data: result }] = useInvokeCodeFunctionMutation(); + +await invoke({ + functionRef: { integrationId, functionId: 'orders-on-update', qualifier: 'prod' }, + payload: { orderId: '6ac54a59212c29912eaddd81', dryRun: true }, + invocationType: 'Sync', +}); +// result: { statusCode, durationMs, output, requestId, logs } + +// The last runs of a trigger (or `{ taskId }` for a scheduled task), newest first. +const { data: runs } = useGetCodeFunctionRunsQuery({ + triggerId: 'trg_1p4R3gTobGZpD6fXvHT4rd', + limit: 20, +}); +``` + +The function side — parsing the envelope your function receives — is +`parseFunctionEvent` in the core SDK (`docs/code-functions.md`), not a hook. +Full method reference: the core SDK's `docs/hub/code.md`. + ## Working with terms A **taxonomy** is a named tree of **terms** (labels). A term can have one parent (a clean hierarchy) or several parents (the same item under many categories). There is one read hook per scenario — pick the one that matches what you want: diff --git a/package-lock.json b/package-lock.json index 764646a..d14f5ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", "@eslint/js": "^10.0.1", - "@norbix.ai/ts": "^4.17.0", + "@norbix.ai/ts": "^4.19.0", "@reduxjs/toolkit": "^2.12.0", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/github": "^12.0.9", @@ -1490,9 +1490,9 @@ } }, "node_modules/@norbix.ai/ts": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/@norbix.ai/ts/-/ts-4.17.0.tgz", - "integrity": "sha512-Wo4DOnyaW91UUz3gbTUyvgxHC8dHPxL4W9FGmdBgPXBNtAgSwReaPuOEqLXXrsQ0F79mkfvrsxQR0ir0BqqbPw==", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@norbix.ai/ts/-/ts-4.19.0.tgz", + "integrity": "sha512-9NhFFaclsXMLy5kMNgSr5ZqiR8P/kWwsOWAfWoVy9hlrOiw5ovPsF5ipK4qmOGbVEzEMtKFYIUUkniNyFWEuOQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 92acf4f..099d08d 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", "@eslint/js": "^10.0.1", - "@norbix.ai/ts": "^4.17.0", + "@norbix.ai/ts": "^4.19.0", "@reduxjs/toolkit": "^2.12.0", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/github": "^12.0.9", diff --git a/src/hooks/hub/code.ts b/src/hooks/hub/code.ts new file mode 100644 index 0000000..95ba659 --- /dev/null +++ b/src/hooks/hub/code.ts @@ -0,0 +1,135 @@ +import type { Norbix } from '@norbix.ai/ts'; + +import type { Builder } from '../index.js'; +import type { Arg, Result } from '../types.js'; + +/** + * `hub.code` — the Code module switch, the Code integrations (AWS Lambda, + * Azure Functions, Google Cloud Functions credentials) and the cloud functions + * they may call: list, aliases / versions, a call by hand, a sample envelope + * and the last runs of a trigger or task. The marketplace half of `hub.code` + * is not wrapped here. + * + * Tag conventions: + * - `Code` — module state + * - `CodeIntegrations/LIST` — integration list + * - `CodeIntegrations/` — one integration + * - `CodeFunctions/LIST` — functions and qualifiers (read live from the provider) + * - `CodeFunctions/RUNS` — last runs + * - `CodeFunctions/SAMPLE` — sample envelopes + * + * The function methods need `@norbix.ai/ts` 4.19.0. + */ +export const hubCode = (b: Builder) => { + type HC = Norbix['hub']['code']; + type Fn = HC[K] extends (...a: never[]) => unknown ? HC[K] : never; + + type EnableModule = Fn<'enableCode'>; + type DisableModule = Fn<'disableCode'>; + type GetIntegrations = Fn<'getCodeIntegrations'>; + type GetIntegration = Fn<'getCodeIntegration'>; + type SaveIntegration = Fn<'saveCodeIntegration'>; + type DeleteIntegration = Fn<'deleteCodeIntegration'>; + type EnableIntegration = Fn<'enableCodeIntegration'>; + type DisableIntegration = Fn<'disableCodeIntegration'>; + type SetIntegrationAsDefault = Fn<'setCodeIntegrationAsDefault'>; + type TestIntegration = Fn<'testCodeIntegration'>; + type GetFunctions = Fn<'getCodeFunctions'>; + type GetQualifiers = Fn<'getCodeFunctionQualifiers'>; + type GetEventSample = Fn<'getCodeFunctionEventSample'>; + type GetRuns = Fn<'getCodeFunctionRuns'>; + type Invoke = Fn<'invokeCodeFunction'>; + + const callHC = (norbix: Norbix) => norbix.hub.code as HC; + + const integrationTags = (arg: unknown) => [ + { type: 'CodeIntegrations' as const, id: 'LIST' }, + { + type: 'CodeIntegrations' as const, + id: (arg as { id?: string; Id?: string })?.id ?? (arg as { Id?: string })?.Id ?? 'CURRENT', + }, + ]; + + return { + // ---- Module toggle ---- + enableCodeModule: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).enableCode(args), + invalidatesTags: ['Account', 'Projects', 'Code'], + }), + disableCodeModule: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).disableCode(args), + invalidatesTags: ['Account', 'Projects', 'Code'], + }), + + // ---- Integrations ---- + getCodeIntegrations: b.query, Arg>({ + query: (args) => (norbix) => callHC(norbix).getCodeIntegrations(args), + providesTags: [{ type: 'CodeIntegrations', id: 'LIST' }], + }), + + getCodeIntegration: b.query, Arg>({ + query: (args) => (norbix) => callHC(norbix).getCodeIntegration(args), + providesTags: (_res, _err, arg) => [ + { type: 'CodeIntegrations', id: (arg as { id?: string })?.id ?? 'CURRENT' }, + ], + }), + + saveCodeIntegration: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).saveCodeIntegration(args), + invalidatesTags: (_res, _err, arg) => integrationTags(arg), + }), + + deleteCodeIntegration: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).deleteCodeIntegration(args), + invalidatesTags: (_res, _err, arg) => integrationTags(arg), + }), + + enableCodeIntegration: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).enableCodeIntegration(args), + invalidatesTags: (_res, _err, arg) => integrationTags(arg), + }), + + disableCodeIntegration: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).disableCodeIntegration(args), + invalidatesTags: (_res, _err, arg) => integrationTags(arg), + }), + + setCodeIntegrationAsDefault: b.mutation< + Result, + Arg + >({ + query: (args) => (norbix) => callHC(norbix).setCodeIntegrationAsDefault(args), + // The default flag moves between integrations: the whole list changes. + invalidatesTags: ['CodeIntegrations'], + }), + + testCodeIntegration: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).testCodeIntegration(args), + }), + + // ---- Cloud functions (core SDK 4.19.0) ---- + getCodeFunctions: b.query, Arg>({ + query: (args) => (norbix) => callHC(norbix).getCodeFunctions(args), + providesTags: [{ type: 'CodeFunctions', id: 'LIST' }], + }), + + getCodeFunctionQualifiers: b.query, Arg>({ + query: (args) => (norbix) => callHC(norbix).getCodeFunctionQualifiers(args), + providesTags: [{ type: 'CodeFunctions', id: 'LIST' }], + }), + + getCodeFunctionEventSample: b.query, Arg>({ + query: (args) => (norbix) => callHC(norbix).getCodeFunctionEventSample(args), + providesTags: [{ type: 'CodeFunctions', id: 'SAMPLE' }], + }), + + getCodeFunctionRuns: b.query, Arg>({ + query: (args) => (norbix) => callHC(norbix).getCodeFunctionRuns(args), + providesTags: [{ type: 'CodeFunctions', id: 'RUNS' }], + }), + + invokeCodeFunction: b.mutation, Arg>({ + query: (args) => (norbix) => callHC(norbix).invokeCodeFunction(args), + }), + }; +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index a371589..997dc85 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -13,6 +13,7 @@ import { apiPublic } from './api/public.js'; import { hubAccount } from './hub/account.js'; import { hubAi } from './hub/ai.js'; import { hubApikeys } from './hub/apikeys.js'; +import { hubCode } from './hub/code.js'; import { hubDatabase } from './hub/database.js'; import { hubEmail } from './hub/email.js'; import { hubEnvironments } from './hub/environments.js'; @@ -143,6 +144,7 @@ export function buildEndpoints(builder: Builder) { ...hubAccount(builder), ...hubAi(builder), ...hubApikeys(builder), + ...hubCode(builder), ...hubMembership(builder), ...hubDatabase(builder), ...hubFiles(builder), diff --git a/tests/code.test.ts b/tests/code.test.ts new file mode 100644 index 0000000..afd1aaf --- /dev/null +++ b/tests/code.test.ts @@ -0,0 +1,256 @@ +import { URL } from 'node:url'; + +import { Norbix } from '@norbix.ai/ts'; +import { describe, expect, it, vi } from 'vitest'; + +import { createNorbixBaseQuery } from '../src/baseQuery.js'; +import { hubCode } from '../src/hooks/hub/code.js'; + +/** + * The `hub.code` hooks — the module switch, the Code integrations and the + * cloud functions (list, qualifiers, invoke, event sample, runs). The hooks are + * a 1:1 wrapper, so the contract is: the hook reaches the right SDK method + * with the caller's arguments, is a query or a mutation as expected, and + * carries the cache tags the dashboard relies on. The last describe drives the + * real `@norbix.ai/ts` client with a fake fetch — never a real server. + */ +function fakeBuilder() { + return { + query: (def: Record) => ({ ...def, __kind: 'query' }), + mutation: (def: Record) => ({ ...def, __kind: 'mutation' }), + } as never; +} + +type Tags = unknown[] | ((res: unknown, err: unknown, arg: unknown) => unknown[]); + +type Def = { + __kind: 'query' | 'mutation'; + query: (args: unknown) => (norbix: unknown) => unknown; + providesTags?: Tags; + invalidatesTags?: Tags; +}; + +/** hook name → [kind, SDK method on norbix.hub.code]. */ +const CODE: Record = { + enableCodeModule: ['mutation', 'enableCode'], + disableCodeModule: ['mutation', 'disableCode'], + getCodeIntegrations: ['query', 'getCodeIntegrations'], + getCodeIntegration: ['query', 'getCodeIntegration'], + saveCodeIntegration: ['mutation', 'saveCodeIntegration'], + deleteCodeIntegration: ['mutation', 'deleteCodeIntegration'], + enableCodeIntegration: ['mutation', 'enableCodeIntegration'], + disableCodeIntegration: ['mutation', 'disableCodeIntegration'], + setCodeIntegrationAsDefault: ['mutation', 'setCodeIntegrationAsDefault'], + testCodeIntegration: ['mutation', 'testCodeIntegration'], + getCodeFunctions: ['query', 'getCodeFunctions'], + getCodeFunctionQualifiers: ['query', 'getCodeFunctionQualifiers'], + getCodeFunctionEventSample: ['query', 'getCodeFunctionEventSample'], + getCodeFunctionRuns: ['query', 'getCodeFunctionRuns'], + invokeCodeFunction: ['mutation', 'invokeCodeFunction'], +}; +const NAMES = Object.keys(CODE); + +function fakeNorbix() { + const calls: Array<{ method: string; args: unknown }> = []; + const record = (method: string) => (args: unknown) => { + calls.push({ method, args }); + return Promise.resolve({ ok: true }); + }; + const code = Object.fromEntries(NAMES.map((n) => [CODE[n]![1], record(CODE[n]![1])])); + return { calls, client: { hub: { code } } }; +} + +function tagsFor(tags: Tags | undefined, arg: unknown): unknown { + return typeof tags === 'function' ? tags(undefined, undefined, arg) : tags; +} + +const endpoints = hubCode(fakeBuilder()) as unknown as Record; + +const INTEGRATION = 'int_6rBACnEaxQ0x1jTiHtAuWV'; + +describe('hubCode', () => { + it('covers the module switch, the 8 integration endpoints and the 5 function endpoints', () => { + expect(Object.keys(endpoints).sort()).toEqual([...NAMES].sort()); + }); + + it.each(NAMES)('%s reaches the matching SDK method with the caller args', async (key) => { + const { calls, client } = fakeNorbix(); + const args = { id: INTEGRATION, integrationId: INTEGRATION }; + + await endpoints[key]!.query(args)(client); + + expect(calls).toEqual([{ method: CODE[key]![1], args }]); + }); + + it.each(NAMES)('%s is a %s', (key) => { + expect(endpoints[key]!.__kind).toBe(CODE[key]![0]); + }); + + it('module switch invalidates Account, Projects and Code', () => { + for (const key of ['enableCodeModule', 'disableCodeModule']) { + expect(tagsFor(endpoints[key]!.invalidatesTags, undefined)).toEqual([ + 'Account', + 'Projects', + 'Code', + ]); + } + }); + + it('the integration list provides CodeIntegrations/LIST; one integration provides CodeIntegrations/', () => { + expect(tagsFor(endpoints['getCodeIntegrations']!.providesTags, {})).toEqual([ + { type: 'CodeIntegrations', id: 'LIST' }, + ]); + expect(tagsFor(endpoints['getCodeIntegration']!.providesTags, { id: INTEGRATION })).toEqual([ + { type: 'CodeIntegrations', id: INTEGRATION }, + ]); + }); + + it.each(['deleteCodeIntegration', 'enableCodeIntegration', 'disableCodeIntegration'])( + '%s invalidates the list and the integration (the SDK spells the path token Id)', + (key) => { + expect(tagsFor(endpoints[key]!.invalidatesTags, { Id: INTEGRATION })).toEqual([ + { type: 'CodeIntegrations', id: 'LIST' }, + { type: 'CodeIntegrations', id: INTEGRATION }, + ]); + }, + ); + + it('saveCodeIntegration invalidates the list and the saved integration (id), or CURRENT on create', () => { + const tags = endpoints['saveCodeIntegration']!.invalidatesTags; + expect(tagsFor(tags, { id: INTEGRATION })).toEqual([ + { type: 'CodeIntegrations', id: 'LIST' }, + { type: 'CodeIntegrations', id: INTEGRATION }, + ]); + expect(tagsFor(tags, { displayName: 'Lambda eu-central-1' })).toEqual([ + { type: 'CodeIntegrations', id: 'LIST' }, + { type: 'CodeIntegrations', id: 'CURRENT' }, + ]); + }); + + it('setCodeIntegrationAsDefault invalidates every CodeIntegrations entry; test invalidates nothing', () => { + expect(tagsFor(endpoints['setCodeIntegrationAsDefault']!.invalidatesTags, {})).toEqual([ + 'CodeIntegrations', + ]); + expect(endpoints['testCodeIntegration']!.invalidatesTags).toBeUndefined(); + }); + + it('functions and qualifiers provide CodeFunctions/LIST, runs RUNS, the sample SAMPLE; invoke invalidates nothing', () => { + expect(tagsFor(endpoints['getCodeFunctions']!.providesTags, {})).toEqual([ + { type: 'CodeFunctions', id: 'LIST' }, + ]); + expect(tagsFor(endpoints['getCodeFunctionQualifiers']!.providesTags, {})).toEqual([ + { type: 'CodeFunctions', id: 'LIST' }, + ]); + expect(tagsFor(endpoints['getCodeFunctionRuns']!.providesTags, {})).toEqual([ + { type: 'CodeFunctions', id: 'RUNS' }, + ]); + expect(tagsFor(endpoints['getCodeFunctionEventSample']!.providesTags, {})).toEqual([ + { type: 'CodeFunctions', id: 'SAMPLE' }, + ]); + expect(endpoints['invokeCodeFunction']!.invalidatesTags).toBeUndefined(); + }); +}); + +/** + * Through the real `@norbix.ai/ts` client and this package's base query with a + * fake fetch: verb, route, where the fields go (query string on GET, JSON body + * on POST), and the project scope headers. + */ +describe('code hooks through the real SDK (fake fetch)', () => { + function run(key: string, args: unknown) { + type FetchFn = typeof globalThis.fetch; + const seen: Array<{ + url: string; + method: string; + body: string | undefined; + headers: InstanceType; + }> = []; + const fetchImpl = vi.fn(async (...[input, init]: Parameters) => { + seen.push({ + url: String(input), + method: (init?.method ?? 'GET').toUpperCase(), + body: typeof init?.body === 'string' ? init.body : undefined, + headers: new globalThis.Headers(init?.headers ?? {}), + }); + return new globalThis.Response(JSON.stringify({ responseStatus: { isSuccess: true } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }) as unknown as FetchFn; + const norbix = new Norbix({ + bearerToken: 'test-token', + projectId: 'test-project', + apiVersion: 'v2', + hubVersion: 'v2', + baseUrl: { api: 'https://api.norbix.io', hub: 'https://hub.norbix.io' }, + fetch: fetchImpl, + }); + const baseQuery = createNorbixBaseQuery(() => norbix); + const result = baseQuery(endpoints[key]!.query(args) as never, {} as never, {}); + return { seen, result }; + } + + const HUB = 'https://hub.norbix.io/v2'; + + it('getCodeFunctions sends GET /code/functions with the filters in the query string', async () => { + const { seen, result } = run('getCodeFunctions', { + integrationId: INTEGRATION, + prefix: 'orders-', + tag: 'norbix=bookshop', + }); + expect(((await result) as { error?: unknown }).error).toBeUndefined(); + expect(seen[0]!.method).toBe('GET'); + const url = new URL(seen[0]!.url); + expect(`${url.origin}${url.pathname}`).toBe(`${HUB}/code/functions`); + expect(url.searchParams.get('integrationId')).toBe(INTEGRATION); + expect(url.searchParams.get('prefix')).toBe('orders-'); + expect(url.searchParams.get('tag')).toBe('norbix=bookshop'); + expect(seen[0]!.headers.get('Authorization')).toBe('Bearer test-token'); + expect(seen[0]!.headers.get('norbix-project-id')).toBe('test-project'); + }); + + it.each([ + [ + 'getCodeFunctionQualifiers', + 'qualifiers', + { integrationId: INTEGRATION, functionId: 'orders-on-update' }, + ], + [ + 'getCodeFunctionEventSample', + 'event-sample', + { source: 'Database', event: 'database.record.updated' }, + ], + ['getCodeFunctionRuns', 'runs', { triggerId: 'trg_1', limit: 5 }], + ])('%s sends GET /code/functions/%s', async (key, segment, args) => { + const { seen, result } = run(key, args); + expect(((await result) as { error?: unknown }).error).toBeUndefined(); + expect(seen[0]!.method).toBe('GET'); + const url = new URL(seen[0]!.url); + expect(`${url.origin}${url.pathname}`).toBe(`${HUB}/code/functions/${segment}`); + for (const [k, v] of Object.entries(args)) expect(url.searchParams.get(k)).toBe(String(v)); + }); + + it('invokeCodeFunction sends POST /code/functions/invoke with the functionRef and payload in the body', async () => { + const args = { + functionRef: { + integrationId: INTEGRATION, + functionId: 'orders-on-update', + qualifier: 'prod', + }, + payload: { orderId: '6ac54a59212c29912eaddd81', dryRun: true }, + invocationType: 'Sync', + }; + const { seen, result } = run('invokeCodeFunction', args); + expect(((await result) as { error?: unknown }).error).toBeUndefined(); + expect(seen[0]!.method).toBe('POST'); + expect(seen[0]!.url).toBe(`${HUB}/code/functions/invoke`); + expect(JSON.parse(seen[0]!.body!)).toEqual(args); + }); + + it('getCodeIntegration sends GET /code/integrations/', async () => { + const { seen, result } = run('getCodeIntegration', { id: INTEGRATION }); + expect(((await result) as { error?: unknown }).error).toBeUndefined(); + expect(seen[0]!.method).toBe('GET'); + expect(seen[0]!.url).toBe(`${HUB}/code/integrations/${INTEGRATION}`); + }); +}); diff --git a/tests/database.test.ts b/tests/database.test.ts index 1cd52dd..25c2c1d 100644 --- a/tests/database.test.ts +++ b/tests/database.test.ts @@ -324,7 +324,7 @@ describe('hub records through the real SDK (fake fetch)', () => { expect(seen[0].method).toBe('GET'); expect(seen[0].url.split('?')[0]).toBe('https://hub.norbix.io/v2/database/collections/orders'); expect(seen[0].headers.get('Authorization')).toBe('Bearer test-token'); - expect(seen[0].headers.get('X-CM-ProjectId')).toBe('test-project'); + expect(seen[0].headers.get('norbix-project-id')).toBe('test-project'); expect(out.data).toEqual({ result: '[{"_id":"r1"}]', totalCount: 1 }); }); diff --git a/tests/files.test.ts b/tests/files.test.ts index 1520e8c..e5f8189 100644 --- a/tests/files.test.ts +++ b/tests/files.test.ts @@ -252,7 +252,7 @@ describe('testFilesIntegrationApi through the real SDK (fake fetch)', () => { expect(seen[0].method).toBe('POST'); expect(seen[0].url).toBe('https://api.norbix.io/v2/files/int_42/test'); expect(seen[0].headers.get('Authorization')).toBe('Bearer test-token'); - expect(seen[0].headers.get('X-CM-ProjectId')).toBe('test-project'); + expect(seen[0].headers.get('norbix-project-id')).toBe('test-project'); expect(out.data?.items.map((i) => `${i.operation}:${i.result}`)).toEqual([ 'UploadFile:OK', 'GetFile:OK', diff --git a/tests/scheduler.test.ts b/tests/scheduler.test.ts index 0e33ee2..bc258bc 100644 --- a/tests/scheduler.test.ts +++ b/tests/scheduler.test.ts @@ -212,7 +212,7 @@ describe('scheduler hooks through the real SDK (fake fetch)', () => { expect(seen[0]!.url).toBe(url); expect(seen[0]!.body).toBeUndefined(); expect(seen[0]!.headers.get('Authorization')).toBe('Bearer test-token'); - expect(seen[0]!.headers.get('X-CM-ProjectId')).toBe('test-project'); + expect(seen[0]!.headers.get('norbix-project-id')).toBe('test-project'); }); it('getSchedulerTasks sends GET /v2/scheduler/tasks with the filters in the query string', async () => { diff --git a/tests/sms.test.ts b/tests/sms.test.ts index 9efd40d..e295aa5 100644 --- a/tests/sms.test.ts +++ b/tests/sms.test.ts @@ -167,7 +167,7 @@ describe('stop / disable-dependencies / render through the real SDK (fake fetch) expect(seen[0]!.method).toBe('POST'); expect(seen[0]!.url).toBe('https://hub.norbix.io/v2/notifications/sms/campaigns/cmp_1/stop'); expect(seen[0]!.headers.get('Authorization')).toBe('Bearer test-token'); - expect(seen[0]!.headers.get('X-CM-ProjectId')).toBe('test-project'); + expect(seen[0]!.headers.get('norbix-project-id')).toBe('test-project'); }); it('getSmsDisableDependencies sends GET /v2/notifications/sms/disable-dependencies and parses the answer', async () => { From 824069ed1e269cf28268f1fcf28c4b16da8d7444 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Mon, 12 Oct 2026 14:22:43 +0300 Subject: [PATCH 2/2] chore(deps): bump handlebars to 4.7.10 (dev, via conventional-changelog-writer) GHSA-8r5x-fm3f-whwj, GHSA-p8wg-vrv2-v86f, GHSA-xw65-4hp5-5hc7: handlebars 4.7.9 -> 4.7.10, dev-only (semantic-release commit analyzer), never shipped in @norbix.ai/react-redux. The OSV scan on the pull request stops at it. Co-Authored-By: Claude Fable 5.1 --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d14f5ee..246904c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5657,13 +5657,13 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.5", + "minimist": "^1.2.8", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0"