{"id":24973401,"url":"https://github.com/thinknimble/tn-models-fp","last_synced_at":"2025-04-11T08:44:24.667Z","repository":{"id":142225750,"uuid":"613007878","full_name":"thinknimble/tn-models-fp","owner":"thinknimble","description":"TS version of tn-models","archived":false,"fork":false,"pushed_at":"2025-03-05T11:59:07.000Z","size":1090,"stargazers_count":2,"open_issues_count":34,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-25T06:13:28.515Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thinknimble.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-12T16:19:43.000Z","updated_at":"2025-03-05T11:58:38.000Z","dependencies_parsed_at":"2023-11-07T20:43:05.305Z","dependency_job_id":"0f53bd64-a797-49d2-8282-9e7276fe5c1a","html_url":"https://github.com/thinknimble/tn-models-fp","commit_stats":null,"previous_names":[],"tags_count":57,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thinknimble%2Ftn-models-fp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thinknimble%2Ftn-models-fp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thinknimble%2Ftn-models-fp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thinknimble%2Ftn-models-fp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thinknimble","download_url":"https://codeload.github.com/thinknimble/tn-models-fp/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248362688,"owners_count":21091177,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-02-03T18:59:14.802Z","updated_at":"2025-04-11T08:44:24.640Z","avatar_url":"https://github.com/thinknimble.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TN Models - FP approach + TS \u003c!-- omit in toc --\u003e\n\nThis package attempts to approach [ tn-models ](https://github.com/thinknimble/tn-models) from a functional paradigm to avoid having issues with types in classes. Thus, preventing doing weird stuff with class static fields which are undiscoverable from TS perspective.\n\nWe also prevent runtime obfuscation of fields ( this happened with classes where a field was forgotten to be declared, runtime would have no clue that the field ever was returned from the API). With this in mind, this package warns users that there is a mismatch between what the model declared was vs what the api actually returned.\n\nThe package is based in zod to replace models and fields approach from previous version\n\n# Table of contents \u003c!-- omit in toc --\u003e\n\n- [Getting started](#getting-started)\n  - [Install this package with your favorite package manager!](#install-this-package-with-your-favorite-package-manager)\n  - [Quickstart](#quickstart)\n  - [Create your api!](#create-your-api)\n  - [Use its built-in methods in your app](#use-its-built-in-methods-in-your-app)\n  - [Create your own calls](#create-your-own-calls)\n- [API reference](#api-reference)\n  - [`createApi`](#createapi)\n    - [Built-in methods](#built-in-methods)\n      - [`create` - Post request to create a resource](#create---post-request-to-create-a-resource)\n      - [`retrieve` - Get request to retrieve a single resource by Id](#retrieve---get-request-to-retrieve-a-single-resource-by-id)\n      - [`list` - Get request to obtain a paginated list of the resource](#list---get-request-to-obtain-a-paginated-list-of-the-resource)\n      - [`update` - Put/Patch request to update a resource by id](#update---putpatch-request-to-update-a-resource-by-id)\n  - [Models (zod-based)](#models-zod-based)\n    - [Why shapes and not zods?](#why-shapes-and-not-zods)\n    - [Make fields readonly](#make-fields-readonly)\n  - [`createCustomServiceCall`](#createcustomservicecall)\n    - [Call with filters](#call-with-filters)\n    - [`standAlone` calls](#standalone-calls)\n    - [On the service callback parameters](#on-the-service-callback-parameters)\n  - [`createPaginatedServiceCall`](#createpaginatedservicecall)\n    - [Add filters to your call](#add-filters-to-your-call)\n    - [Build a uri dynamically](#build-a-uri-dynamically)\n  - [`createApiUtils`](#createapiutils)\n  - [`createCollectionManager`](#createcollectionmanager)\n    - [Example use](#example-use)\n- [Roadmap](#roadmap)\n- [Contribution guide](#contribution-guide)\n  - [pnpm](#pnpm)\n  - [Tests](#tests)\n  - [Side by side debugging](#side-by-side-debugging)\n  - [Publishing new version of the package.](#publishing-new-version-of-the-package)\n\n# Getting started\n\n## Install this package with your favorite package manager!\n\n```bash\nnpm i @thinknimble/tn-models-fp\n```\n\n```bash\nyarn add @thinknimble/tn-models-fp\n```\n\n```bash\npnpm i @thinknimble/tn-models-fp\n```\n\n## Quickstart\n\n```typescript\n/**\n *\n *  // creating a simple user api with login, registration, update\n *\n */\n\nimport axios from \"axios\"\nimport { z } from \"zod\"\nimport { GetInferredFromRaw, createCustomServiceCall } from \"@thinknimble/tn-models\"\n\n/**\n * The entity is the default type that is used as an output shape to the prebuilt methods\n */\nconst userEntity = {\n  id: z.string().uuid(),\n  firstName: z.string(),\n  lastName: z.string(),\n  email: z.string(),\n  token: z.string(),\n}\n\n/**\n * Defining a create shape because the registration api expects a different object\n */\n\nconst createShape = {\n  email: z.string(),\n  password: z.string(),\n  firstName: z.string(),\n  lastName: z.string(),\n}\n/**\n * Since I know my update method can be partial and wont include all the same fields as a create I create an update shape\n */\n\nconst updateShape = {\n  firstName: z.string().optional(),\n  lastName: z.string().optional(),\n}\n\n/**\n * login only requires an email and password\n */\n\nconst loginShape = {\n  email: z.string(),\n  password: z.string(),\n}\n\n/**\n * Create your api\n * Each api has a create, retrieve, list method by default\n * They must be enabled by declaring an `entity` in the `models` field \n *\n * These methods are accessible through the api directly eg:\n * userApi.create({})\n * userApi.retrieve({})\n * userApi.list()\n *\n */\n\n/**\n * create additional methods using the createCustomServiceCall provider\n * these methods are accesible with the shorthand name for csc\n * e.g userApi.csc.login({})\n */\n\nconst customUpdate = createCustomServiceCall(\n  {\n    inputShape: partialUpdateShape,\n    outputShape: accountShape,\n    cb: async ({ client, slashEndingBaseUri, input, utils: { toApi, fromApi } }) =\u003e {\n    const { id, ...rest } = toApi(input)\n    const res = await client.patch(`${slashEndingBaseUri}${id}/`, rest)\n    return fromApi(res.data)\n    }\n  },\n)\n\nconst login = createCustomServiceCall(\n  {\n    inputShape: loginShape,\n    outputShape: accountShape,\n    cb: async ({ client, slashEndingBaseUri, input, utils: { toApi, fromApi } }) =\u003e {\n      const data = toApi(input)\n      const res = await client.post(`api/login/`, rest)\n      return fromApi(res.data)\n    } \n  },\n)\n\n/**\n * There is no need for an output shape in this case\n */\nconst deleteEntity = createCustomServiceCall(\n  {\n    inputShape: z.string().uuid(),\n    cb: async ({ client, slashEndingBaseUri, input }) =\u003e {\n      const res = await client.delete(`api/users/${input}/`)\n      return\n    } \n  },\n)\n\nconst userApi = createApi({\n  client: axios.create(), // a client of your choice\n  baseUri: \"api/users/\", // a base URI to be used as a default\n  models: {\n    /**\n     * if I do not declare any overrides for the three default methods this will be used\n     */\n    entity: accountShape,\n    /**\n     *\n     * Pass a `create` model if you want to override the default input type, otherwise, just passing an entity will generate a default create\n     * In order to customize the output shape of the default methods you must create a custom call (createCustomServiceCall). That would only be necessary if your declared entity shape type is not what the creation request responds with\n     *\n     * */\n\n    create: createShape,\n  },\n  customCalls:{\n    // Additional (aka custom calls) methods are declared here\n    login, update, deleteEntity\n  }\n})\n\n/**\n * finally use your api with your favorite wrapper or directly\n */\n\n/**\n * This is a utility from TN-Models that is used to return a TS type from the zod shape\n * The type can be used anywhere in the code and removes the need for creating one manually\n */\ntype User = GetInferredFromRaw\u003ctypeof scheduleRequestInputShape\u003e\n\nlet user: User | null = null\n\ntry {\n  const user = userApi.create({ email: \"test@test.com\", password: \"password\", firstName: \"first\", lastName: \"last\" })\n  const res = userApi.csc.login({ email: \"random@random.com\", password: \"iamapassword\" })\n  const userAfterLogin = res.data\n} catch (e) {\n  console.log(e)\n}\n```\n\n## Create your api!\n\nYou need a couple of things:\n\n- An `AxiosInstance`. Either create it on the fly or provide an existing one\n- A base uri for your api ( don't worry about trailing slashes we take care of that)\n- Models (all optional):\n  - Model for resource entity: shape of the resource that will be returned by the api. Declaring this model will grant you access to [ built-in methods ](#built-in-methods)\n  - Model for the input of resource creation (optional, grants you access to `create` built-in method)\n\nIG:\n\n```typescript\nimport axios from \"axios\"\nimport { z } from \"zod\"\n\nconst createShape = {\n  completed: z.boolean().default(false),\n  content: z.string().min(1),\n  completedDate: z.string().datetime().nullable(),\n}\n\nexport const todoApi = createApi({\n  client: axios.create(),\n  baseUri: \"api/todo\",\n  models: {\n    create: createShape,\n    entity: { id: z.string().uuid(), ...createShape },\n  },\n})\n```\n\n## Use its built-in methods in your app\n\n```typescript\nimport {todoApi} from './services'\nimport {useMutation} from '@tanstack/react-query'\nimport {useState} from 'react'\nimport { Pagination } from '@thinknimble/tn-models-fp'\n\nconst TodoManager = () =\u003e {\n  const [selectedTodoId,setSelectedTodoId] = useState()\n\n/**\n * Please note the use of TanStack is not required!\n */\n  const {data: selectedTodo} = useQuery({\n    queryKey: ['todo',selectedTodoId],\n    queryFn: () =\u003e todoApi.retrieve( selectedTodoId )\n  })\n\n  const {mutateAsync:create} = useMutation({\n    mutationFn: todoApi.create\n  })\n\n  const [pagination, setPagination] = useState( new Pagination({ page:1 }) )\n  const { data:currentList } = useQuery({\n    queryKey: ['todo-list',page],\n    queryFn: () =\u003e todoApi.list( { pagination } )\n  })\n\n  return (\n    //...\n  )\n}\n\n```\n\n## Create your own calls\n\nWe include a field: `customCalls` in `createApi` that can be passed so you can create your own calls.\nTo do this you pass an object with the service callbacks. These should be created with `createCustomServiceCall` method:\n\nThe `createCustomServiceCall` method takes the models for your input and output shapes of the call. Then there's a `cb` field callback that is powered up with multiple arguments that provide you with all the tools we think you need to make a type-safe call\n\n```typescript\nconst updatePartial = createCustomServiceCall(\n  {\n    inputShape: partialUpdateShape,\n    outputShape: entityShape,\n    cb: async ({ client, slashEndingBaseUri, input, utils: { toApi, fromApi } }) =\u003e {\n      const { id, ...rest } = toApi(input)\n      const res = await client.patch(`${slashEndingBaseUri}${id}`, rest)\n      return fromApi(res.data)\n    }\n  },\n)\n```\n\n# API reference\n\n## `createApi`\n\nThis is the main entrypoint to tn-models.\n\nAn api handler can be created with or without custom service calls. Any custom call is provided with a set of utils accordingly to what they're told what the input-output is. These utils allow to convert camelCase-\u003esnake_case (toApi) as well as snake_case-\u003ecamelCase (fromApi).\n\nThe result of the function call is an object that allows to do type-safe calls to the given api. It follows as closely as possible the same api as the class-based version of the library.\n\n### Built-in methods\n\nWhen passing an `entity` model to `createApi` parameter you get a couple of built-in methods as a result.\n\n#### `create` - Post request to create a resource\n\nIf you passed a `create` model you would get the input type resolved from that shape. Otherwise `entity` will be used as a default\n\nNote that if `entity` shape is used to resolve the type of the input, any [readonly fields](#make-fields-readonly) and `id` itself will be stripped from that type. So\n\n```typescript\nconst entityShape = {\n  id: z.string().uuid(),\n  firstName: z.string(),\n  lastName: z.string(),\n  fullName: z.string().readonly(),\n}\nconst api = createApi({\n  //...\n  models: {\n    entity: entityShape,\n  },\n})\n```\n\nWill result in the following call signature for the create method:\n\n```typescript\napi.create({\n  //id: stripped because it is considered readonly regardless of whether you declared it readonly or not\n  firstName: \"sample first name\",\n  lastName: \"sample last name\",\n  //fullName: stripped because it is a readonly-declared field\n})\n```\n\n#### `retrieve` - Get request to retrieve a single resource by Id\n\nReturns the resolved type of `entity` from given models.\n\n#### `list` - Get request to obtain a paginated list of the resource\n\nNote: Please check that your backend uses the ListMixin in the targetted resource, otherwise this method will not be useful for you.\n\nThis method translates as a `GET` call to the `baseUri/` uri.\n\nReturns a paginated version of the resolved type of `entity` from given models.\n\n#### `update` - Put/Patch request to update a resource by id\n\nThis method takes as parameter the resolved type of the `entity` from given models minus the [declared readonly fields](#make-fields-readonly) which are stripped to keep you from sending them in the request.\n\nThere are a couple of flavors of this method to your convenience:\n\n- `update(partialResource)` does a patch request with a partial body\n- `update.replace(fullResource)` does a put request with a full body\n- `update.replace.asPartial(partialResource)` does a put request with a partial body\n\n## Models (zod-based)\n\n[`zod`](https://zod.dev/) works as a validation library but it also provides a good set of type utilities that can be used to narrow, infer or define typescript types.\n\nThis library sees zod as a library that bridges the gap between typescript world and javascript world. In other words, compile-time and run-time. For this reason it was determined that it would fit perfectly for fulfilling the role of models in this new approach.\n\nZod is going to be used both as the core tool for our type inference and as a validator parser (for snake_casing requests and camelCasing responses as well as checking whether the type received from api is the same as expected).\n\nWhat we're using in this approach (and what we would require users to use) are zod raw shapes. Which in plain words are objects which values are\n\n- `ZodTypeAny` : pretty much anything that you can create with zod's `z`\n\nSample models:\n\n```ts\nimport { z } from \"zod\"\n\nconst createShape = {\n  //ZodRawShape\n  completed: z.boolean().default(false), //ZodBoolean\n  content: z.string().min(1), // ZodString\n  completedDate: z.string().datetime().nullable(), // ZodString\n  extraInformation: z.object({\n    // ZodObject\n    developerUserId: z.string().uuid(), //...\n    reviewerUserId: z.string().uuid(),\n    qaUserId: z.string().uuid(),\n    prDetails: z.object({\n      url: z.string().url(),\n    }),\n  }),\n}\n\nconst entityShape = { ...createZodRaw, id: z.number() }\n```\n\n### Why shapes and not zods?\n\nUsually when using zod we directly create a zod schema (in any of their forms) but here we would like to be a step before the schema itself.\n\nThe reason for this decision was based on the fact that we're going to need to convert our schemas from/to camel/snake case. If we were to create a zod schema (object) we would render the shape inaccessible which would deter us from being able to swap its keys to another casing style.\n\nIG:\n\n```ts\nconst myZodObject = z.object({\n  // zod schema\n  dateOfBirth: z.string().datetime(),\n  email: z.string(),\n  age: z.number(),\n}) // after declaration, the shape cannot be retrieved\n\nconst myZodShape = {\n  //zod shape\n  dateOfBirth: z.string().datetime(),\n  email: z.string(),\n  age: z.number(),\n} // asking for the shape allow us to do what we please with its keys and later simply call `z.object` internally when we need the zod schema\n```\n\n\u003e PS: In a not-that-far future we want to accept zod objects as well as shapes. Or even eradicate shapes altogether, since ZodObjects are the go-to for creating schemas with zod.\n\n### Make fields readonly\n\nYou can mark fields as readonly with the built-in zod's `.readonly()` function. This will use a `z.ZodReadonly` for your field which will allow the library to identify it as a readonly field, thus preventing those fields to be included in models for creation and update.\n\n```ts\nconst entityShape = {\n  firstName: z.string(),\n  lastName: z.string(),\n  fullName: z.string().readonly(),\n}\n```\n\n## `createCustomServiceCall`\n\nThis function is used as a complement to `createApi` and allows us to create custom service calls attached to the api.\n\nWe provided multiple overloads for it to be fully type-safe and properly infer the parameters for you.\n\nWithout this function, you cannot add custom service calls. This was designed as to enforce the type safety of the custom calls. If you're looking for a way to self-host one of these calls please check [`standAlone` calls](#standalone-calls)\n\n\u003cdetails\u003e\n\u003csummary\u003eExample\u003c/summary\u003e\n\n```typescript\n// from tn-models-client sample repo\nconst deleteTodo = createCustomServiceCall(\n  {\n    inputShape: z.number(), //define your input shape (in this case is a ZodPrimitive)\n    cb: async ({ input, client, slashEndingBaseUri }) =\u003e {\n      //you get your parsed input, the axios client and the base uri you defined in `createApi`\n      await client.delete(`${slashEndingBaseUri}${input}`)\n    }\n  },\n)\n\nconst updatePartial = createCustomServiceCall(\n  {\n    inputShape: partialUpdateZodRaw, //you can also pass `ZodRawShape`s\n    outputShape: entityZodRaw,\n    cb:   async ({ client, slashEndingBaseUri, input, utils: { toApi, fromApi } }) =\u003e {\n      // we provide util methods to convert from and to api within your custom call so have you them in handy to use here.\n      const { id, ...rest } = toApi(input)\n      const res = await client.patch(`${slashEndingBaseUri}${id}`, rest)\n      return fromApi(res.data)\n    }\n  },\n)\n```\n\n\u003c/details\u003e\n\nTo add these custom calls to your created api you simply pass them as object to the `customCalls` field\n\nIG (same as first createApi example but with custom calls)\n\n```ts\nexport const todoApi = createApi(\n  {\n    client,\n    baseUri,\n    models: {\n      create: createZodRaw,\n      entity: entityZodRaw,\n    },\n    customCalls: {\n      // object with declared custom service calls\n      deleteTodo,\n      updatePartial,\n    }\n  }\n)\n```\n\nWe also added a `csc` alias in case you feel `customServiceCall` is too long.\n\n### Call with filters\n\nTo make calls that include query params a `filtersShape` object has to be added in the first parameter of `createCustomServiceCall`. This enables the resulting service call function to include a filter parameter to have readily available the filters to pass them as parameter or parse them in your own way:\n\n\u003cdetails\u003e\n\u003csummary\u003eExample\u003c/summary\u003e\n\n```typescript\nconst callWithFilter = createCustomServiceCall(\n  {\n    inputShape,\n    outputShape,\n    filtersShape: {\n      testFilter: z.string(),\n      testArrayFilter: z.string().array(),\n    }\n    // `parsedFilters` contains your filters ready to be used in the uri. They're snake cased so expect `test_filter` and `test_array_filter`,\n    cb: async ({ client, slashEndingBaseUri, parsedFilters }) =\u003e {\n      const result = await client.get(slashEndingBaseUri, { params: parsedFilters })\n      return result.data\n    }\n  },\n)\n\nconst api = createApi(\n  {\n    client,\n    baseUri,\n    customCalls:{\n      callWithFilter,\n    }\n  }\n)\nawait api.csc.callWithFilter({\n  input: { testInput: \"testInput\" },\n  filters: {\n    testFilter: \"test\",\n    testArrayFilter: [1, 22],\n  },\n})\n```\n\n\u003c/details\u003e\n\n### `standAlone` calls\n\nThere could be situations where you don't want to attach a call to an api. Probably a one-off request or an rpc-like request which is not attached to a specific resource.\n\nFor this case we can use `createCustomServiceCall.standAlone` which is a function that gets fed a `client` and is a self-contained version of regular custom service calls.\n\nHere's an example\n\n```typescript\nconst standAloneCall = createCustomServiceCall.standAlone({\n  client: axios,\n  models: {\n    outputShape: {\n      testData: z.string(),\n    },\n    inputShape: {\n      testInput: z.number(),\n    },\n  },\n  name: \"standAlone\",\n  cb: async ({\n    client,\n    utils,\n    input,\n    //!! You don't have the uri available here since this is self hosted there's no other context than this, you'll have to provide the full uri in your api request\n    // slashEndingBaseUri,\n  }) =\u003e {\n    const res = await client.post(\"/api/my-endpoint/\", utils.toApi(input))\n    return utils.fromApi(res.data)\n  },\n})\n```\n\n### On the service callback parameters\n\nWe provide a set of parameters in the custom service callback:\n\n- `client`: a type-wrapped axios instance that makes sure you call the apis with slash ending uris.\n\nFor this client to consume your uri strings you should either cast them `as const` or define them as template strings directly in the call\n\n```typescript\n  client.get(`${slashEndingBaseUri}`) // slashEndingBaseUri is an `as const` variable\n  client.get(`${slashEndingBaseUri}/ending/`) // ✅ define the template string directly in the function call\n\n  const uriSampleOutsideOfCall =`${slashEndingBaseUri}my-uri/not-const/`\n  client.get(uriSample)// ❌ this does not check, you'll get error, template string is already evaluated outside so it is considered `string`\n\n  const uriSampleOutsideOfCallAsConst = `${slashEndingBaseUri}my-uri/not-const/` as const\n  client get(uriSampleOutsideOfCallAsConst)//✅ was cast as const outside of the call\n```\n\n- `slashEndingBaseUri`: gives you a reference to the base uri you passed when you created the api so you can use it within the callback\n- `input`:the parsed input based on the `inputShape` you passed\n- `utils`: set of utilities to convert from and to api models (handles object casing)\n  - `fromApi`: convert a response object from the api (coming in snake casing) to its camelCase version\n  - `toApi`: convert an input into an snake_cased object so that you can feed it to the api.\n- `parsedFilters`: only available if you provided a `filtersShape` when constructing the custom call. This yields the filters ready-to-go(parsed to string and snake cased!) into the `params` field of the opts parameter of axios\n\n## `createPaginatedServiceCall`\n\nAllows users to create paginated calls that are not directly related with the `GET` `baseUri/` endpoint of their resource. Such as when an endpoint retrieves a paginated list of things that are not exactly the resource ig: a search.\n\nIG\n\n```typescript\nconst getMatches = createPaginatedServiceCall(\n  {\n    // inputShape: someInputShape (optional)\n    outputShape: entityZodShape,\n    opts: {\n      uri: \"get-matches\",\n      // httpMethod: 'post' (optional, default get)\n    }\n  }\n)\n\nconst api = createApi(\n  //...models\n  customCalls:{\n    getMatches\n  }\n)\n```\n\n### Add filters to your call\n\nAdd filters to calls by passing a `filtersShape` in the first parameter. This will allow to pass filters in the resulting service call function\n\n\u003cdetails\u003e\n\u003csummary\u003eExample\u003c/summary\u003e\n\n```typescript\nconst paginatedCallWithFilters = createPaginatedServiceCall({\n  outputShape,\n  filtersShape: {\n    myExtraFilter: z.string(),\n    anotherExtraFilter: z.number(),\n  },\n})\nconst api = createApi(\n  {\n    baseUri,\n    client,\n    customCalls:{\n      paginatedCallWithFilters,\n\n    }\n  }\n)\nconst pagination = new Pagination({ page: 1, size: 20 })\nconst myExtraFilter = \"test\"\n//act\nawait api.csc.paginatedCallWithFilters({\n  input: {\n    pagination,\n  },\n  filters: {\n    myExtraFilter, // besides the pagination qparams this will also pass my_extra_filter as a query param\n  },\n})\n```\n\n\u003c/details\u003e\n\n### Build a uri dynamically\n\nYou can create paginated calls and have their uri to be dynamic.\n\nPrevious example simply showed a static uri but adding a `urlParams` to the `inputShape` would result in `uri` to be a builder function:\n\n\u003cdetails\u003e\n\u003csummary\u003eExample\u003c/summary\u003e\n\n```typescript\nconst callWithUrlParams = createPaginatedServiceCall(\n  {\n    inputShape: {\n      urlParams: z.object({\n        someId: z.string(),\n      }),\n    },\n    outputShape,\n    opts: {\n      uri: ({ someId }) =\u003e `myUri/${someId}`,\n    }\n  }\n)\nconst api = createApi(\n  {\n    baseUri,\n    client,\n    customCalls:{\n      callWithUrlParams,\n    }\n  }\n)\nconst pagination = new Pagination({ page: 1, size: 20 })\nconst randomId = faker.datatype.uuid()\nawait api.csc.callWithUrlParams({ pagination, urlParams: { someId: randomId } }) // requests myUri/${randomId}\n```\n\n\u003c/details\u003e\n\n## `createApiUtils`\n\nBefore using this please see [`createCustomServiceCall.standAlone`](#createcustomservicecall)\n\nThis is mostly an internal function and we would recommend using custom calls that have this util included under the hood. If custom calls don't suit your need then this is exported for your convenience\n\nThis util allows to create the utils independently without the need of creating the api.\n\nThis is useful especially for creating remote procedure calls where no resource is strictly attached and an action is being triggered ig: call to send an email\n\n## `createCollectionManager`\n\nCreates a collection manager (intended to be) equivalent to the existing class `CollectionManager` util.\nThe only required parameter is the `fetchList` field, which expects a reference from your `list` function in the created api.\n\n### Example use\n\n```typescript\nconst api = createApi({\n  //...\n})\nconst collectionManager = createCollectionManager({\n  fetchList: api.list,\n  list: [], // your feed list, type-inferred from api.list\n  pagination: feedPagination, // your pagination object\n  filters: feedFilters, // inferred from api.list\n})\n```\n\n# Roadmap\n\nCheck out the [ Issues tab ](https://github.com/thinknimble/tn-models-fp/issues) for incoming features and feature/request.\n\nSubmit your own if you feel there's something we're missing!\n\n# Contribution guide\n\n## pnpm\n\nWe're using pnpm while developing this library. You can easily get setup with it by doing\n\n```shell\nnpm i -g pnpm\n```\n\n## Tests\n\nWe always make sure that our PRs pass all current tests and it is highly recommended to at least add a set of tests that are related with the work of the given PR so that we get a minimum level of confidence that things are working fine.\n\nTo run tests we have a dev command which will scan the whole repo and run the tests it finds.\n\n```shell\npnpm test:dev\n```\n\nWe can also isolate tests so that we tackle one at a time by adding `.only` calls to the test suite (`describe`) and/or the test itself (`it`). There's also the chance to run a single file as test\n\nRun a single test file\n\n```shell\npnpm test:dev ./src/api/tests/create-api.test.ts\n```\n\nIsolate a test suite/test with `only`:\n\n```typescript\n//some.test.ts\ndescribe.only(\"some test suite\", () =\u003e {\n  it.only(\"tests some functionality\", () =\u003e {\n    //...\n  })\n  //...\n})\n```\n\n## Side by side debugging\n\nIf you want to debug this library from an external application, you can run the watch command (below) so any changes would generate a new build, that is it will generate the lib files in `/dist`\n\n```\npnpm run dev:watch\n```\n\nThese build files enable us to refer to this package in an external app's `package.json` as a file:\n\n```json\n// package.json of external application\n//...\ndependencies:{\n  //...\n  \"@thinknimble/tn-models\":\"file:../path/to/tn-models\"\n  //...\n}\n//...\n```\n\nIf you don't have an app and want a very simple one where you can test out tn-models you can check out this one which we set up for this sole purpose: [tn-models-script-app](https://github.com/thinknimble/tn-models-script-app). Please follow the README in there to get the app set up.\n\n## Publishing new version of the package.\n\nTo make our life easier with package versioning and releases we're using [changeset](https://github.com/changesets/changesets/tree/main).\n\nIf a PR for a feature conveys a release with it OR you want to release a version after some PRs have been merged.\n\nFrom the root of the project you have to\n\n```\npnpm changeset\n```\n\nFollow the prompts:\n\n- Give it a patch/minor/major depending on the release you mean to publish.\n- Add a description of what the release contains.\n\nCommit those changes into the PR or create a PR for it and merge it.\n\nWhat this will do is create a Version release PR that will allow you to confirm the release. Once that PR is merged, the github action will reach out to npm and publish the package for you.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthinknimble%2Ftn-models-fp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthinknimble%2Ftn-models-fp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthinknimble%2Ftn-models-fp/lists"}