Skip to content

Commit

Permalink
Merge pull request #19 from proofgeist/v3
Browse files Browse the repository at this point in the history
V3
  • Loading branch information
eluce2 authored Apr 25, 2023
2 parents c694a79 + 35a403f commit 4ac9307
Show file tree
Hide file tree
Showing 28 changed files with 5,497 additions and 475 deletions.
8 changes: 8 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
4 changes: 3 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ module.exports = {
sourceType: "module",
},
plugins: ["react", "@typescript-eslint"],
rules: {},
rules: {
"@typescript-eslint/no-var-requires": "off",
},
};
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
node_modules
dist
dist-browser
dist-tokenStore
schema
.env.local
fmschema.config.js
Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @proofgeist/fmdapi

## 3.0.0

### Major Changes

- 5c2f0d2: Use native fetch (Node 18+).

This package now requires Node 18+ and no longer relys on the `node-fetch` package.
Each method supports passing additional options to the `fetch` function via the `fetch` parameter. This is useful if used within a framework that overrides the global `fetch` function (such as Next.js).

### Minor Changes

- 5c2f0d2: Custom functions to override where the temporary access token is stored
- add LocalStorage and Upstash helper methods for token store
118 changes: 90 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ This package is designed to make working with the FileMaker Data API much easier
- [Otto](https://ottofms.com/) Data API proxy support
- TypeScript support for easy auto-completion of your fields
- Automated type generation based on layout metadata
- Supports both node and edge runtimes (v3.0+)

## Installation

This library requires zod as a peer depenency and Node 18 or later

```sh
npm install @proofgeist/fmdapi
# or
yarn add @proofgeist/fmdapi
npm install @proofgeist/fmdapi zod
```

```sh
yarn add @proofgeist/fmdapi zod
```

## Usage

Add the following envnironment variables to your project's `.env.local` file:
Add the following envnironment variables to your project's `.env` file:

```sh
FM_DATABASE=filename.fmp12
Expand Down Expand Up @@ -54,15 +59,18 @@ Then, use the client to query your FileMaker database. Availble methods:
- `create` return a new record
- `update` modify a single record by recordID
- `delete` delete a single record by recordID
- `executeScript` execute a FileMaker script direclty
- `layouts` return a list of all layouts in the database
- `scripts` return a list of all scripts in the database
- `metadata` return metadata for a given layout
- `disconnect` forcibly logout of your FileMaker session (available when not using Otto Data API proxy)

This package also includes some helper methods to make working with Data API responses a little easier:

- `findOne` return the first record from a find instead of an array. This method will error unless exactly 1 record is found.
- `findFirst` return the first record from a find instead of an array, but will not error if multiple records are found.

...more helper methods planned
- `findAll` return all found records from a find, automatically handling pagination. Use caution with large datasets!
- `listAll` return all records from a given layout, automatically handling pagination. Use caution with large datasets!

Basic Example:

Expand All @@ -72,12 +80,13 @@ const result = await client.list({ layout: "Contacts" });

### Client Setup Options

| Option | Type | Description |
| -------- | -------- | -------------------------------------------------------------------------------------------- |
| `auth` | `object` | Authentication object. Must contain either `apiKey` or `username` and `password` |
| `db` | `string` | FileMaker database name |
| `server` | `string` | FileMaker server URL (must include `https://`) |
| `layout` | `string` | _(optional)_ If provided, will be the layout used for all methods if not otherwise specified |
| Option | Type | Description |
| ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `auth` | `object` | Authentication object. Must contain either `apiKey` or `username` and `password` |
| `db` | `string` | FileMaker database name |
| `server` | `string` | FileMaker server URL (must include `https://`) |
| `layout` | `string` | _(optional)_ If provided, will be the layout used for all methods if not otherwise specified |
| `tokenStore` | `TokenStore` | _(optional)_ If provided, will use the custom set of functions to store and retrieve the short-lived access token. This only used for the username/password authenication method. See below for more details |

## TypeScript Support

Expand All @@ -92,11 +101,58 @@ type TContact = {
const result = await client.list<TContact>({ layout: "Contacts" });
```

💡 TIP: For a more ergonomic TypeScript experience, use the [included codegen tool](#automatic-type-generation) to generate these types based on your FileMaker layout metadata.

## Custom Token Store (v3.0+)

If you are using username/password authentication, this library will manage your access token for you. By default, the token is kept in memory only, but you can provide other getter and setter methods to store the token in a database or other location. Included in this package are helper functions for file storage if you have access to the filesystem, or Upstash if running in a serverless environment.

```typescript
import { DataApi } from "@proofgeist/fmdapi";

// using file storage
import { fileTokenStore } from "@proofgeist/fmdapi/dist/tokenStore/file";
const client = DataApi({
auth: {
username: process.env.FM_USERNAME,
password: process.env.FM_PASSWORD,
},
db: process.env.FM_DATABASE,
server: process.env.FM_SERVER,
tokenStore: fileTokenStore(),
});

// or with Upstash, requires `@upstash/redis` as peer dependency
import { upstashTokenStore } from "@proofgeist/fmdapi/dist/tokenStore/upstash";
const client = DataApi({
auth: {
username: process.env.FM_USERNAME,
password: process.env.FM_PASSWORD,
},
db: process.env.FM_DATABASE,
server: process.env.FM_SERVER,
tokenStore: upstashTokenStore({
token: process.env.UPSTASH_TOKEN,
url: process.env.UPSTASH_URL,
}),
});
```

## Edge Runtime Support (v3.0+)

Since version 3.0 uses the native `fetch` API, it is compatible with edge runtimes, but there are some additional considerations to avoid overwhelming your FileMaker server with too many connections. If you are developing for the edge, be sure to implement one of the following strategies:

- ✅ Use a custom token store (see above) with a persistent storage method such as Upstash
- ✅ Use a proxy such as the [Otto Data API Proxy](https://www.ottofms.com/docs/otto/working-with-otto/proxy-api-keys/data-api) which handles management of the access tokens itself.
- Providing an API key to the client instead of username/password will automatically use the Otto proxy
- ⚠️ Call the `disconnect` method on the client after each request to avoid leaving open sessions on your server
- this method works, but is not recommended in most scenarios as it reuires a new session to be created for each request

## Automatic Type Generation

This package also includes a helper function that will automatically generate types for each of your layouts. Use this tool regularly during development to easily keep your types updated with any schema changes in FileMaker. 🤯

The generated file also produces a layout-specific client instance that will automatically type all of the methods for that layout **and** validates the response using the [`zod`](https://github.com/colinhacks/zod) library. This validaiton happens at runtime so you can protect against dangerous field changes even when you haven't ran the code generator recently, or in your projection deployment!
The generated file also produces a layout-specific client instance that will automatically type all of the methods for that layout **and** validates the response using the [`zod`](https://github.com/colinhacks/zod) library. This validaiton happens at runtime so you can protect against dangerous field changes even when you haven't ran the code generator recently, or in your production deployment!

### Setup instructions:

Expand All @@ -113,18 +169,20 @@ yarn codegen --init
yarn codegen
```

Assuming you have a layout called `customer_api` containing `name` `phone` and `email` fields for a customer, the generated code will look like this:
Assuming you have a layout called `customer_api` containing `name` `phone` and `email` fields for a customer, the generated code will look something like this:

```typescript
// schema/Customer.ts
import { z } from "zod";
import { DataApi } from "@proofgeist/fmdapi";
export const ZCustomer = z.object({
name: z.string(),
phone: z.string(),
email: z.string(),
});
export type TCustomer = z.infer<typeof ZCustomer>;

// schema/client/Customer.ts
import { DataApi } from "@proofgeist/fmdapi";
export const client = DataApi<any, TCustomer>(
{
auth: { apiKey: process.env.OTTO_API_KEY },
Expand All @@ -139,20 +197,28 @@ export const client = DataApi<any, TCustomer>(
You can use the exported types to type your own client, or simply use the generated client to get typed and validated results, like so:

```ts
import { client } from "schema/Customer";
import { CustomerClient } from "schema/client";
...
const result = await client.list(); // result will be fully typed and validated!
const result = await CustomerClient.list(); // result will be fully typed and validated!
```

#### `generateSchemas` options

| Option | Type | Default | Description |
| -------------- | ---------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| envNames | `object` | _undefined_ | This object has the same structure as the client config parameters and is used to overrride the environment variable names used for the generated client. |
| schemas | `Schema[]` | _(required)_ | An array of `Schema` objects to generate types for (see below) |
| path | `string` | `"./schema"` | Path to folder where generated files should be saved. |
| generateClient | `boolean` | `true` | Will generate a layout-specific typed client for you. Set to `false` if you only want to generate the types. |
| useZod | `boolean` | `true` | When enabled, will generate Zod schema in addition to TypeScript types and add validation to the generated client for each layout |
| Option | Type | Default | Description |
| -------------- | ---------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| envNames | `object` | _undefined_ | This object has the same structure as the client config parameters and is used to overrride the environment variable names used for the generated client. |
| schemas | `Schema[]` | _(required)_ | An array of `Schema` objects to generate types for (see below) |
| path | `string` | `"./schema"` | Path to folder where generated files should be saved. |
| generateClient | `boolean` | `true` | Will generate a layout-specific typed client for you. Set to `false` if you only want to generate the types. |
| useZod | `boolean` | `true` | When enabled, will generate Zod schema in addition to TypeScript types and add validation to the generated client for each layout |
| tokenStore | `function` | `memoryStore` | A [custom token store](#custom-token-store-v30) function to be included in the generated client |

In order to support whatever token store you may import, all import statements from your config file will be copied into the generated client files by default. To exclude certain imports, add a comment containing `codegen-ignore` in the line above the import statement you wish to exclude. e.g.

```ts
// codegen-ignore
import something from "whatever";
```

#### `Schema` options

Expand All @@ -170,10 +236,6 @@ const result = await client.list(); // result will be fully typed and validated!

They are just files added to your project, so you technically can, but we don't recommend it, as it would undermine the main benefit of being able to re-run the script at a later date when the schema changes—all your edits would be overritten. If you need to extend the types, it's better to do extend them into a new type/zod schema in another file. Or, if you have suggesstions for the underlying engine, Pull Requests are welcome!

### Do I have to install `zod` for this package to work?

No, but we reccomend it! Zod is included as a depenency with this package and used under the hood when you use the generated layout-specific client to validate the responses from your server. If you wish to disable the validation, you can pass `useZod: false` to the generated client.

### Why are number fields typed as a `string | number`?

FileMaker may return numbers as strings in certain cases (such as very large numbers in scientific notation or blank fields). This ensures you properly account for this in your frontend code. If you wish to force all numbers to be typed as `number | null`, you can enable the `strictNumbers` flag per schema in your definition.
Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const config: Config.InitialOptions = {
testEnvironment: "node",
verbose: true,
automock: false,
setupFiles: ["./test/setup.js"],
};

export default config;
28 changes: 21 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,48 +1,62 @@
{
"name": "@proofgeist/fmdapi",
"version": "2.2.14",
"version": "3.0.0",
"description": "FileMaker Data API client",
"main": "dist/index.js",
"repository": "[email protected]:proofgeist/fm-dapi.git",
"author": "Eric <[email protected]>",
"license": "MIT",
"private": false,
"exports": {
".": "./dist/index.js",
"./tokenStore": "./dist/tokenStore"
},
"bin": {
"codegen": "dist/cli.js"
},
"scripts": {
"prepublishOnly": "tsc",
"build": "tsc",
"test": "jest"
"test": "jest",
"changeset": "changeset"
},
"dependencies": {
"@changesets/cli": "^2.26.1",
"chalk": "4.1.2",
"commander": "^9.2.0",
"dayjs": "^1.11.2",
"dotenv": "^16.0.1",
"fs-extra": "^10.1.0",
"node-fetch": "2.6.7",
"ts-toolbelt": "^9.6.0",
"zod": "^3.21.4"
"ts-toolbelt": "^9.6.0"
},
"peerDependencies": {
"zod": ">=3.21.4"
},
"devDependencies": {
"@types/fs-extra": "^9.0.13",
"@types/jest": "^28.1.3",
"@types/node": "^17.0.21",
"@types/node-fetch": "^2.6.1",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"@upstash/redis": "^1.20.4",
"eslint": "^8.34.0",
"eslint-plugin-react": "^7.32.2",
"jest": "^28.1.1",
"nock": "^13.2.4",
"jest-fetch-mock": "^3.0.3",
"ts-jest": "^28.0.5",
"ts-node": "^10.7.0",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"zod": ">=3.21.4"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"src",
"dist",
"dist-browser",
"tokenStore",
"utils",
"stubs"
],
Expand Down
5 changes: 2 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ async function runCodegen({ configLocation }: ConfigArgs) {
return process.exit(1);
});

// eslint-disable-next-line @typescript-eslint/no-var-requires
const config: GenerateSchemaOptions = require(configLocation);
await generateSchemas(config).catch((err) => {
const config: GenerateSchemaOptions<unknown> = await import(configLocation);
await generateSchemas(config, configLocation).catch((err) => {
console.error(err);
});
console.log(`✅ Generated schemas\n`);
Expand Down
Loading

0 comments on commit 4ac9307

Please sign in to comment.