From bac0ca7e1acfb3ef185b1cac38bc7c6549ad7732 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Tue, 7 May 2024 12:46:00 -0400 Subject: [PATCH] feat: Fully support all path-to-regex features in RestEndpoint.path types (#3054) --- .changeset/few-carpets-cheat.md | 5 + .changeset/violet-elephants-grin.md | 18 ++++ .circleci/config.yml | 3 +- packages/rest/package.json | 4 +- packages/rest/src/__tests__/RestEndpoint.ts | 2 +- packages/rest/src/pathTypes.ts | 27 +++--- packages/rest/tsconfig.test.json | 15 +++ packages/rest/tsconfig.typetest.json | 12 --- .../types.test.ts | 92 ++++++++++++++++++- .../editor-types/@data-client/rest.d.ts | 2 +- .../Playground/editor-types/globals.d.ts | 8 +- 11 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 .changeset/few-carpets-cheat.md create mode 100644 .changeset/violet-elephants-grin.md create mode 100644 packages/rest/tsconfig.test.json delete mode 100644 packages/rest/tsconfig.typetest.json rename packages/rest/{src/__tests__ => typescript-tests}/types.test.ts (88%) diff --git a/.changeset/few-carpets-cheat.md b/.changeset/few-carpets-cheat.md new file mode 100644 index 000000000000..72986e925973 --- /dev/null +++ b/.changeset/few-carpets-cheat.md @@ -0,0 +1,5 @@ +--- +"@data-client/rest": patch +--- + +Support + and \* in RestEndpoint.path diff --git a/.changeset/violet-elephants-grin.md b/.changeset/violet-elephants-grin.md new file mode 100644 index 000000000000..a154c3381446 --- /dev/null +++ b/.changeset/violet-elephants-grin.md @@ -0,0 +1,18 @@ +--- +"@data-client/rest": patch +--- + +Add support for {} to RestEndpoint.path + + +```ts + const getThing = new RestEndpoint({ + path: '/:attr1?{-:attr2}?{-:attr3}?', + }); + + getThing({ attr1: 'hi' }); + getThing({ attr2: 'hi' }); + getThing({ attr3: 'hi' }); + getThing({ attr1: 'hi', attr3: 'ho' }); + getThing({ attr2: 'hi', attr3: 'ho' }); + ``` \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index e05f566b8581..bead4008f0d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -71,8 +71,7 @@ jobs: at: ~/ - run: command: | - cd packages/endpoint - yarn run typecheck + yarn workspaces foreach -A --include @data-client/endpoint --include @data-client/rest run typecheck unit_tests: parameters: diff --git a/packages/rest/package.json b/packages/rest/package.json index bedd85dd9204..1bc6b583b883 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -120,7 +120,9 @@ "build": "run build:lib && run build:legacy:lib && run build:bundle", "dev": "run build:lib -w", "prepare": "run build:lib", - "prepack": "run prepare && run build:bundle && run build:legacy:lib" + "prepack": "run prepare && run build:bundle && run build:legacy:lib", + "tsc:ci": "yarn g:tsc --project tsconfig.test.json", + "typecheck": "run tsc:ci" }, "author": "Nathaniel Tucker (https://github.com/ntucker)", "funding": "https://github.com/sponsors/ntucker", diff --git a/packages/rest/src/__tests__/RestEndpoint.ts b/packages/rest/src/__tests__/RestEndpoint.ts index 483708160c01..2f34f84efa9e 100644 --- a/packages/rest/src/__tests__/RestEndpoint.ts +++ b/packages/rest/src/__tests__/RestEndpoint.ts @@ -2,7 +2,7 @@ import { Entity, schema } from '@data-client/endpoint'; import { useController } from '@data-client/react'; import { useSuspense } from '@data-client/react'; import { CacheProvider } from '@data-client/react'; -import { Article, CoolerArticle, CoolerArticleResource } from '__tests__/new'; +import { CoolerArticle, CoolerArticleResource } from '__tests__/new'; import nock from 'nock'; import { makeRenderDataClient } from '../../../test'; diff --git a/packages/rest/src/pathTypes.ts b/packages/rest/src/pathTypes.ts index cb6a202f3f07..22515b32e3ff 100644 --- a/packages/rest/src/pathTypes.ts +++ b/packages/rest/src/pathTypes.ts @@ -1,28 +1,33 @@ -type OnlyOptional = S extends `${infer K}?` ? K : never; +type OnlyOptional = + S extends `${infer K}}?` ? K + : S extends `${infer K}?` ? K + : never; type OnlyRequired = S extends `${string}?` ? never : S; +/** Parameters for a given path */ +export type PathArgs = + PathKeys extends never ? + // unknown is identity for intersection ('&') + unknown + : KeysToArgs>; + /** Computes the union of keys for a path string */ export type PathKeys = string extends S ? string - : S extends `${infer A}\\:${infer B}` ? PathKeys | PathKeys - : S extends `${infer A}\\?${infer B}` ? PathKeys | PathKeys + : S extends `${infer A}\\${':' | '?' | '+' | '*' | '{' | '}'}${infer B}` ? + PathKeys | PathKeys : PathSplits; type PathSplits = - S extends `${string}:${infer K}${'/' | ',' | '%' | '&'}${infer R}` ? + S extends ( + `${string}:${infer K}${'/' | ',' | '%' | '&' | '+' | '*' | '{'}${infer R}` + ) ? PathSplits<`:${K}`> | PathSplits : S extends `${string}:${infer K}:${infer R}` ? PathSplits<`:${K}`> | PathSplits<`:${R}`> : S extends `${string}:${infer K}` ? K : never; -/** Parameters for a given path */ -export type PathArgs = - PathKeys extends never ? - // unknown is identity for intersection ('&') - unknown - : KeysToArgs>; - export type KeysToArgs = { [K in Key as OnlyOptional]?: string | number; } & (OnlyRequired extends never ? unknown diff --git a/packages/rest/tsconfig.test.json b/packages/rest/tsconfig.test.json new file mode 100644 index 000000000000..6edd3404d088 --- /dev/null +++ b/packages/rest/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false, + "skipLibCheck": true, + "noImplicitAny": false, + "composite": false, + "rootDir": "../..", + "paths": { + "__tests__/*": ["__tests__/*"] + } + }, + "include": ["typescript-tests"] +} diff --git a/packages/rest/tsconfig.typetest.json b/packages/rest/tsconfig.typetest.json deleted file mode 100644 index 43563b40bc6b..000000000000 --- a/packages/rest/tsconfig.typetest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "rootDir": "../..", - "skipDefaultLibCheck": true, - "skipLibCheck": true, - "jsx": "preserve", // this is just to make it work with older typescript versions in our tests - "emitDeclarationOnly": false, - "noEmit": true - }, - "include": ["./src/next/__tests__/types.test.ts", "./src/__tests__/types.test.ts", "./src/*", "./src/next/*", "../../__tests__"], -} diff --git a/packages/rest/src/__tests__/types.test.ts b/packages/rest/typescript-tests/types.test.ts similarity index 88% rename from packages/rest/src/__tests__/types.test.ts rename to packages/rest/typescript-tests/types.test.ts index 137c3bb4e0c1..ceb1af5a67fc 100644 --- a/packages/rest/src/__tests__/types.test.ts +++ b/packages/rest/typescript-tests/types.test.ts @@ -3,8 +3,8 @@ import { Entity, schema } from '@data-client/endpoint'; import { useController, useSuspense } from '@data-client/react'; import { User } from '__tests__/new'; -import createResource from '../createResource'; -import RestEndpoint, { GetEndpoint, MutateEndpoint } from '../RestEndpoint'; +import createResource from '../src/createResource'; +import RestEndpoint, { GetEndpoint, MutateEndpoint } from '../src/RestEndpoint'; it('RestEndpoint construct and extend with typed options', () => { new RestEndpoint({ @@ -574,3 +574,91 @@ it('should handle more open ended type definitions', () => { explicit.push(); }; }); + +() => { + const getThing = new RestEndpoint({ + path: '/:id*:bob', + }); + + getThing({ id: 5, bob: 'hi' }); + // @ts-expect-error + getThing({ id: 'hi' }); + // @ts-expect-error + getThing({ bob: 'hi' }); + // @ts-expect-error + getThing(5); +}; +() => { + const getThing = new RestEndpoint({ + path: '/:id+:bob', + }); + + getThing({ id: 5, bob: 'hi' }); + // @ts-expect-error + getThing({ 'id+': 5, bob: 'hi' }); + // @ts-expect-error + getThing({ id: 'hi' }); + // @ts-expect-error + getThing({ bob: 'hi' }); + // @ts-expect-error + getThing(5); +}; +() => { + const getThing = new RestEndpoint({ + path: '/:id\\+:bob', + }); + + getThing({ id: 5, bob: 'hi' }); + // @ts-expect-error + getThing({ 'id+': 5, bob: 'hi' }); + // @ts-expect-error + getThing({ id: 'hi' }); + // @ts-expect-error + getThing({ bob: 'hi' }); + // @ts-expect-error + getThing(5); +}; +() => { + const getThing = new RestEndpoint({ + path: '/:id:bob+', + }); + + getThing({ id: 5, bob: 'hi' }); + // @ts-expect-error + getThing({ id: 5, 'bob+': 'hi' }); + // @ts-expect-error + getThing({ id: 'hi' }); + // @ts-expect-error + getThing({ bob: 'hi' }); + // @ts-expect-error + getThing(5); +}; +() => { + const getThing = new RestEndpoint({ + path: '/:foo/(.*)', + }); + + getThing({ foo: 'hi' }); + // @ts-expect-error + getThing({}); + // @ts-expect-error + getThing({ id: 'hi' }); + // @ts-expect-error + getThing(5); +}; +() => { + const getThing = new RestEndpoint({ + path: '/:attr1?{-:attr2}?{-:attr3}?', + }); + + getThing({ attr1: 'hi' }); + getThing({ attr2: 'hi' }); + getThing({ attr3: 'hi' }); + getThing({ attr1: 'hi', attr3: 'ho' }); + getThing({ attr2: 'hi', attr3: 'ho' }); + getThing({}); + // @ts-expect-error + getThing({ random: 'hi' }); + // @ts-expect-error + getThing(5); +}; diff --git a/website/src/components/Playground/editor-types/@data-client/rest.d.ts b/website/src/components/Playground/editor-types/@data-client/rest.d.ts index 587cf0720248..a2a0b466ed39 100644 --- a/website/src/components/Playground/editor-types/@data-client/rest.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/rest.d.ts @@ -1054,7 +1054,7 @@ type ExtractCollection = S extends ({ type OnlyOptional = S extends `${infer K}?` ? K : never; type OnlyRequired = S extends `${string}?` ? never : S; /** Computes the union of keys for a path string */ -type PathKeys = string extends S ? string : S extends `${infer A}\\:${infer B}` ? PathKeys | PathKeys : S extends `${infer A}\\?${infer B}` ? PathKeys | PathKeys : PathSplits; +type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '?'}${infer B}` ? PathKeys | PathKeys : PathSplits; type PathSplits = S extends `${string}:${infer K}${'/' | ',' | '%' | '&'}${infer R}` ? PathSplits<`:${K}`> | PathSplits : S extends `${string}:${infer K}:${infer R}` ? PathSplits<`:${K}`> | PathSplits<`:${R}`> : S extends `${string}:${infer K}` ? K : never; /** Parameters for a given path */ type PathArgs = PathKeys extends never ? unknown : KeysToArgs>; diff --git a/website/src/components/Playground/editor-types/globals.d.ts b/website/src/components/Playground/editor-types/globals.d.ts index b083bcaf77e4..6dab5b594382 100644 --- a/website/src/components/Playground/editor-types/globals.d.ts +++ b/website/src/components/Playground/editor-types/globals.d.ts @@ -1055,13 +1055,13 @@ type ExtractCollection = S extends ({ [K: string]: Schema; } ? ExtractObject : never; -type OnlyOptional = S extends `${infer K}?` ? K : never; +type OnlyOptional = S extends `${infer K}}?` ? K : S extends `${infer K}?` ? K : never; type OnlyRequired = S extends `${string}?` ? never : S; -/** Computes the union of keys for a path string */ -type PathKeys = string extends S ? string : S extends `${infer A}\\:${infer B}` ? PathKeys | PathKeys : S extends `${infer A}\\?${infer B}` ? PathKeys | PathKeys : PathSplits; -type PathSplits = S extends `${string}:${infer K}${'/' | ',' | '%' | '&'}${infer R}` ? PathSplits<`:${K}`> | PathSplits : S extends `${string}:${infer K}:${infer R}` ? PathSplits<`:${K}`> | PathSplits<`:${R}`> : S extends `${string}:${infer K}` ? K : never; /** Parameters for a given path */ type PathArgs = PathKeys extends never ? unknown : KeysToArgs>; +/** Computes the union of keys for a path string */ +type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '?' | '+' | '*' | '{' | '}'}${infer B}` ? PathKeys | PathKeys : PathSplits; +type PathSplits = S extends (`${string}:${infer K}${'/' | ',' | '%' | '&' | '+' | '*' | '{'}${infer R}`) ? PathSplits<`:${K}`> | PathSplits : S extends `${string}:${infer K}:${infer R}` ? PathSplits<`:${K}`> | PathSplits<`:${R}`> : S extends `${string}:${infer K}` ? K : never; type KeysToArgs = { [K in Key as OnlyOptional]?: string | number; } & (OnlyRequired extends never ? unknown : {