npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fm-jsdriver

v1.0.2

Published

Type-safe FileMaker driver for webviewer communication using fm-gofer

Readme

fm-jsDriver

A type-safe TypeScript driver for interacting with FileMaker databases through webviewer contexts using the fm-gofer package.

Overview

fm-jsDriver enables JavaScript/TypeScript applications running in a FileMaker webviewer to communicate with FileMaker databases in a type-safe, promise-based manner. It translates high-level method calls into FileMaker Data API format and uses fm-gofer to handle communication via FileMaker.PerformScript.

Key Features

  • Type-safe operations: Full TypeScript support with auto-generated types from your schema
  • Promise-based API: Modern async/await syntax for all operations
  • Schema-driven: Automatic type generation from your FileMaker schema
  • CRUD operations: Create, read, update, delete with minimal boilerplate
  • Script execution: Execute FileMaker scripts with pre/post hooks
  • Webviewer-only: No HTTP/REST API required - all communication through FileMaker webviewer
  • RecordId-based: Uses FileMaker's internal recordId for all operations

How It Works

  1. Communication is ONLY through FileMaker webviewer (no HTTP/REST API)
  2. Uses fm-gofer package for promise-based FileMaker script execution
  3. All operations use FileMaker's internal recordId as handles (not primary keys)
  4. Primary keys are auto-generated by FileMaker and returned in responses
  5. All methods communicate via FMGofer.PerformScript('jsDriver', JSON.stringify(parameter))

Installation

npm install fm-jsdriver

Quick Start

Step 1: Create your schema file

Create a fmSchema.json file that describes your FileMaker database structure:

{
  "fmSchema": {
    "layouts": {
      "Contacts": {
        "fields": {
          "id": {
            "_meta": {
              "autoEnter": true,
              "notEmpty": true,
              "type": "normal"
            },
            "type": "number"
          },
          "name": {
            "_meta": {
              "notEmpty": true,
              "type": "normal"
            },
            "type": "string"
          },
          "email": {
            "_meta": {
              "type": "normal"
            },
            "type": "string"
          },
          "company": {
            "type": "string"
          }
        }
      }
    }
  }
}

Step 2: Generate TypeScript types

npx fm-generate-schema fmSchema.json fmSchema.ts

This generates a fmSchema.ts file with TypeScript interfaces for all your layouts.

Step 3: Use the driver

import { createDriver } from 'fm-jsdriver';
import schema from './fmSchema';

// Create the driver
const fm = createDriver(schema);

// Create a new record
const newContact = await fm.Contacts.create({
  name: 'John Doe',
  email: '[email protected]',
  company: 'Acme Corp'
});

// Get a record by recordId
const contact = await fm.Contacts.get(newContact.recordId);

// Update a record
await fm.Contacts.update(newContact.recordId, {
  company: 'New Company Inc.'
});

// Find records
const results = await fm.Contacts.find({
  company: 'Acme Corp'
}, { limit: 10, offset: 1 });

// List all records
const allContacts = await fm.Contacts.list({ limit: 50 });

// Delete a record
await fm.Contacts.delete(newContact.recordId);

Schema Setup

Understanding fmSchema.json

The schema file describes your FileMaker database structure with type information:

{
  "fmSchema": {
    "layouts": {
      "LayoutName": {
        "fields": {
          "fieldName": {
            "_meta": {
              "autoEnter": boolean,    // Auto-entered field (like primary keys)
              "notEmpty": boolean,      // Required field
              "type": "normal"          // Field type
            },
            "type": "string | number | boolean | date | time | timestamp"
          }
        }
      }
    }
  }
}

Field Metadata

  • autoEnter: true: Field is automatically populated by FileMaker (e.g., primary keys)
  • notEmpty: true: Field is required
  • autoEnter: true + notEmpty: true: Identifies primary key fields

Field Types

Supported TypeScript types:

  • string - Text fields
  • number - Numeric fields
  • boolean - Boolean fields
  • date - Date fields
  • time - Time fields
  • timestamp - Timestamp fields

Type Generation

The CLI tool fm-generate-schema generates TypeScript types from your schema:

npx fm-generate-schema [input-file] [output-file]

Default: npx fm-generate-schema fmSchema.json fmSchema.ts

Generated file includes:

  • TypeScript interfaces for each layout
  • Primary key identification
  • Field documentation with metadata
  • Complete schema export

Example generated interface:

export interface ContactsFields {
  /** auto-enter, required */
  id: number;
  /** required */
  name: string;
  email?: string;
  company?: string;
}

export const ContactsPrimaryKey: string[] = ['id'];

API Reference

createDriver(schema: FMSchema): Driver

Creates a driver instance with layout-specific methods.

import { createDriver } from 'fm-jsdriver';
import schema from './fmSchema';

const fm = createDriver(schema);

Layout Methods

Each layout in your schema exposes these methods:

create(fieldData, prescript?, script?)

Creates a new record. Primary keys are auto-generated by FileMaker.

const result = await fm.Contacts.create({
  name: 'John Doe',
  email: '[email protected]'
});
// Returns: { recordId: '123', fieldData: { id: 1, name: 'John Doe', ... } }

Parameters:

  • fieldData: T - Field values for the new record
  • prescript?: ScriptInput - Optional script to run before creation
  • script?: ScriptInput - Optional script to run after creation

update(recordId, fieldData, prescript?, script?)

Updates an existing record by FileMaker's internal recordId.

await fm.Contacts.update('123', {
  company: 'New Company Inc.'
});

Parameters:

  • recordId: string | number - FileMaker's internal record ID
  • fieldData: Partial<T> - Fields to update (partial)
  • prescript?: ScriptInput - Optional script to run before update
  • script?: ScriptInput - Optional script to run after update

get(recordId, prescript?, script?)

Retrieves a single record by FileMaker's internal recordId.

const contact = await fm.Contacts.get('123');

Parameters:

  • recordId: string | number - FileMaker's internal record ID
  • prescript?: ScriptInput - Optional script to run before retrieval
  • script?: ScriptInput - Optional script to run after retrieval

find(query, options?, prescript?, script?)

Finds records matching query criteria with optional pagination.

const results = await fm.Contacts.find({
  company: 'Acme Corp'
}, {
  offset: 1,
  limit: 10
});

Parameters:

  • query: Partial<T> - Search criteria
  • options?: FindOptions - Pagination options
    • offset?: number - Starting record (default: 1)
    • limit?: number - Maximum records (default: 100)
  • prescript?: ScriptInput - Optional script to run before find
  • script?: ScriptInput - Optional script to run after find

list(options?, prescript?, script?)

Lists all records with optional pagination.

const all = await fm.Contacts.list({
  offset: 1,
  limit: 50
});

Parameters:

  • options?: FindOptions - Pagination options
  • prescript?: ScriptInput - Optional script to run before list
  • script?: ScriptInput - Optional script to run after list

delete(recordId, prescript?, script?)

Deletes a record by FileMaker's internal recordId.

await fm.Contacts.delete('123');

Parameters:

  • recordId: string | number - FileMaker's internal record ID
  • prescript?: ScriptInput - Optional script to run before deletion
  • script?: ScriptInput - Optional script to run after deletion

executeScript(script)

Executes a custom FileMaker script without layout context.

await fm.executeScript({
  script: 'GenerateReport',
  parameter: JSON.stringify({ type: 'monthly' }),
  option: 5
});

// Or with shorthand
await fm.executeScript('GenerateReport');

Parameters:

  • script: ScriptInput - Script name (string) or script object

ScriptInput Type

Scripts can be specified as a string or object:

// String format (converted to object)
'ScriptName'

// Object format
{
  script: 'ScriptName',
  parameter?: 'optional parameter',
  option?: 0
}

Usage Patterns

Basic CRUD Operations

import { createDriver } from 'fm-jsdriver';
import schema from './fmSchema';

const fm = createDriver(schema);

// CREATE
const newContact = await fm.Contacts.create({
  name: 'John Doe',
  email: '[email protected]',
  company: 'Acme Corp'
});

console.log(`Created record with ID: ${newContact.recordId}`);

// READ - Get single record
const contact = await fm.Contacts.get(newContact.recordId);

// READ - Find records
const acmeContacts = await fm.Contacts.find({
  company: 'Acme Corp'
});

// READ - List all
const allContacts = await fm.Contacts.list({ limit: 100 });

// UPDATE
await fm.Contacts.update(newContact.recordId, {
  company: 'New Company Inc.',
  email: '[email protected]'
});

// DELETE
await fm.Contacts.delete(newContact.recordId);

Using Scripts

Execute scripts before and after operations:

// With pre-script and post-script
await fm.Contacts.create(
  {
    name: 'Jane Doe',
    email: '[email protected]'
  },
  { script: 'ValidateData', parameter: 'strict' }, // prescript
  { script: 'SendNotification', parameter: 'new_contact' } // post-script
);

// Execute standalone script
const reportResult = await fm.executeScript({
  script: 'GenerateReport',
  parameter: JSON.stringify({
    type: 'monthly',
    year: 2024
  }),
  option: 5
});

Pagination

Handle large datasets with pagination:

// Get first 50 records
const page1 = await fm.Contacts.list({
  offset: 1,
  limit: 50
});

// Get next 50 records
const page2 = await fm.Contacts.list({
  offset: 51,
  limit: 50
});

// Find with pagination
const results = await fm.Contacts.find({
  company: 'Acme Corp'
}, {
  offset: 1,
  limit: 20
});

Error Handling

try {
  const contact = await fm.Contacts.get('123');
  console.log(contact);
} catch (error) {
  if (error.message.includes('not found')) {
    console.error('Contact not found');
  } else if (error.message.includes('FMGofer is not available')) {
    console.error('Not running in FileMaker webviewer');
  } else {
    console.error('FileMaker error:', error);
  }
}

React Integration

import React, { useState, useEffect } from 'react';
import { createDriver } from 'fm-jsdriver';
import schema from './fmSchema';

const fm = createDriver(schema);

function ContactsList() {
  const [contacts, setContacts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadContacts();
  }, []);

  async function loadContacts() {
    try {
      const result = await fm.Contacts.list({ limit: 50 });
      setContacts(result.response.data);
    } catch (error) {
      console.error('Failed to load contacts:', error);
    } finally {
      setLoading(false);
    }
  }

  async function createContact(data) {
    const newContact = await fm.Contacts.create(data);
    setContacts([...contacts, newContact.response.data[0]]);
  }

  async function updateContact(recordId, data) {
    await fm.Contacts.update(recordId, data);
    loadContacts(); // Refresh list
  }

  async function deleteContact(recordId) {
    await fm.Contacts.delete(recordId);
    setContacts(contacts.filter(c => c.recordId !== recordId));
  }

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h1>Contacts</h1>
      {contacts.map(contact => (
        <div key={contact.recordId}>
          {contact.fieldData.name} - {contact.fieldData.email}
          <button onClick={() => deleteContact(contact.recordId)}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

FileMaker Setup

Required FileMaker Script

You must create a FileMaker script named jsDriver that handles the Execute Data API requests:

# Script Name: jsDriver
# Parameter: JSON string containing DriverParameter

Set Variable [ $parameter ; Value: Get ( ScriptParameter ) ]
Set Variable [ $result ; Value: ExecuteDataAPI ( $parameter ) ]
Exit Script [ Text Result: $result ]

The script receives a JSON parameter in this format:

{
  "prescript": {
    "script": "PreScriptName",
    "parameter": "parameter value",
    "option": 0
  },
  "dapi": {
    "action": "create|read|update|delete",
    "layouts": "LayoutName",
    "recordId": "123",
    "fieldData": { "field1": "value1" },
    "query": [{ "field1": "value1" }],
    "offset": 1,
    "limit": 100
  },
  "script": {
    "script": "PostScriptName",
    "parameter": "parameter value",
    "option": 0
  }
}

FileMaker Script Implementation

Complete implementation of the jsDriver script:

# jsDriver Script

# Get the parameter
Set Variable [ $param ; Value: Get ( ScriptParameter ) ]

# Parse prescript if exists
If [ JSONGetElement ( $param ; "prescript" ) ≠ "" ]
    Set Variable [ $prescriptName ; Value: JSONGetElement ( $param ; "prescript.script" ) ]
    Set Variable [ $prescriptParam ; Value: JSONGetElement ( $param ; "prescript.parameter" ) ]
    Set Variable [ $prescriptOption ; Value: JSONGetElement ( $param ; "prescript.option" ) ]
    Perform Script [ Specified: By Name ; $prescriptName ; Parameter: $prescriptParam ]
End If

# Execute Data API request if exists
If [ JSONGetElement ( $param ; "dapi" ) ≠ "" ]
    Set Variable [ $dapiResult ; Value: ExecuteDataAPI ( JSONGetElement ( $param ; "dapi" ) ) ]
End If

# Parse post-script if exists
If [ JSONGetElement ( $param ; "script" ) ≠ "" ]
    Set Variable [ $scriptName ; Value: JSONGetElement ( $param ; "script.script" ) ]
    Set Variable [ $scriptParam ; Value: JSONGetElement ( $param ; "script.parameter" ) ]
    Set Variable [ $scriptOption ; Value: JSONGetElement ( $param ; "script.option" ) ]
    Perform Script [ Specified: By Name ; $scriptName ; Parameter: $scriptParam ]
End If

# Return result
Exit Script [ Text Result: $dapiResult ]

Type Safety

fm-jsDriver provides complete type safety:

Compile-time Type Checking

// TypeScript knows the exact fields for each layout
await fm.Contacts.create({
  name: 'John',
  email: '[email protected]',
  invalidField: 'error' // TypeScript error: Property 'invalidField' does not exist
});

// Required fields are enforced
await fm.Contacts.create({
  email: '[email protected]' // TypeScript error: Property 'name' is missing
});

// Updates accept partial data
await fm.Contacts.update('123', {
  company: 'New Company' // Only updating company is fine
});

Auto-complete Support

Your IDE will provide full autocomplete for:

  • Layout names
  • Field names
  • Field types
  • Method parameters

Runtime Validation

The schema loader validates:

  • Schema structure
  • Layout definitions
  • Field definitions
  • Required fields

Advanced Usage

Complex Queries

// Find with multiple criteria
const results = await fm.Contacts.find({
  company: 'Acme Corp',
  email: '@acme.com'
}, {
  limit: 50
});

Transaction Patterns

// Use scripts to implement transaction-like behavior
async function createContactWithValidation(contactData) {
  return await fm.Contacts.create(
    contactData,
    { script: 'BeginTransaction' },     // prescript
    { script: 'CommitTransaction' }     // post-script
  );
}

Batch Operations

// Create multiple records
const contacts = [
  { name: 'John Doe', email: '[email protected]' },
  { name: 'Jane Smith', email: '[email protected]' },
  { name: 'Bob Wilson', email: '[email protected]' }
];

const results = await Promise.all(
  contacts.map(contact => fm.Contacts.create(contact))
);

Custom Script Execution

// Execute complex FileMaker scripts
const result = await fm.executeScript({
  script: 'ProcessBatchData',
  parameter: JSON.stringify({
    operation: 'import',
    source: 'csv',
    file: 'data.csv',
    mapping: {
      'Column1': 'name',
      'Column2': 'email'
    }
  }),
  option: 5
});

LLM Integration Guide

This section is specifically designed for AI assistants to understand how to generate code using fm-jsDriver.

Reading the Schema

When a user provides or you read a fmSchema.json or fmSchema.ts file:

  1. Identify Layouts: Look at fmSchema.layouts keys for layout names
  2. Identify Fields: For each layout, examine fields for available field names
  3. Identify Primary Keys: Fields with _meta.autoEnter: true and _meta.notEmpty: true are primary keys
  4. Identify Required Fields: Fields with _meta.notEmpty: true (but not autoEnter) are required
  5. Identify Field Types: Use the type property for TypeScript typing

Code Generation Patterns

Pattern 1: Basic CRUD

// When user asks to "create a contact"
const newContact = await fm.Contacts.create({
  name: 'provided value',
  email: 'provided value'
  // Include all required fields from schema
});

// When user asks to "get contact with recordId 123"
const contact = await fm.Contacts.get('123');

// When user asks to "update contact"
await fm.Contacts.update('123', {
  // Only include fields being updated
  company: 'new value'
});

// When user asks to "delete contact"
await fm.Contacts.delete('123');

Pattern 2: Searching

// When user asks to "find contacts at Acme Corp"
const results = await fm.Contacts.find({
  company: 'Acme Corp'
}, {
  limit: 50  // Always specify limit
});

// When user asks to "list all contacts"
const all = await fm.Contacts.list({
  limit: 100  // Default to reasonable limit
});

Pattern 3: With Scripts

// When user mentions validation or scripts
await fm.Contacts.create(
  { /* field data */ },
  { script: 'ValidateContact' },  // prescript
  { script: 'SendWelcomeEmail' }  // post-script
);

Pattern 4: React Components

// When user asks for React integration
function Component() {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function load() {
      const result = await fm.LayoutName.list();
      setData(result.response.data);
    }
    load();
  }, []);

  async function handleCreate(formData) {
    await fm.LayoutName.create(formData);
    // Refresh data
  }

  return (/* JSX */);
}

Inference Rules

  1. Always use recordId: Never use primary key fields for get/update/delete - only recordId
  2. Include required fields: Check schema for _meta.notEmpty: true fields
  3. Use Partial for updates: Updates should use Partial<T> type
  4. Add pagination: The FileMaker Data API returns a maximum of 100 records per request by default when no limit is specified. Use foundCount and returnedCount in dataInfo to either render a list with pagination or load all records as batches
  5. Type-safe field names: Only use field names that exist in the schema
  6. Handle async: All operations return Promises - use async/await
  7. Error handling: Wrap operations in try/catch for production code
  8. Boolean fields: FileMaker doesn't support boolean fields. Instead of boolean fields of true/false, create a number field of 1/0
  9. Primary keys: FileMaker is responsible for creating primary keys. When creating a new record, fm-jsdriver will return the primary key value in the field designated as primary key
  10. Working with dates and timestamps: FileMaker can return date and timestamp fields in ISO format, but it doesn't support the format for setting data (create or update). To set a date, use the FileMaker accepted format which is YYYY+MM+DD and for timestamp YYYY+MM+DD HH:MM:SS. Timezone issues will be handled in FileMaker
  11. Multiple values: When needing to add multiple values/keys in a field e.g. Apple, Banana, use new line character, which is a natively supported way of storing multiple values in FileMaker (multikey)

Example Conversation Flow

User: "Create a new contact named John Doe with email [email protected]"

LLM:
1. Check fmSchema for Contacts layout
2. Identify required fields (name is required from _meta.notEmpty: true)
3. Generate:

const newContact = await fm.Contacts.create({
  name: 'John Doe',
  email: '[email protected]'
});

Testing

Mocking fm-gofer

For unit tests, mock the FMGofer global:

// test-setup.ts
global.FMGofer = {
  PerformScript: jest.fn().mockResolvedValue('{"response": {}}')
};

// test.ts
import { createDriver } from 'fm-jsdriver';
import schema from './fmSchema';

test('creates contact', async () => {
  const fm = createDriver(schema);
  await fm.Contacts.create({ name: 'Test' });

  expect(FMGofer.PerformScript).toHaveBeenCalledWith(
    'jsDriver',
    expect.stringContaining('"action":"create"')
  );
});

Testing Parameter Transformation

test('transforms create parameters correctly', async () => {
  const fm = createDriver(schema);
  await fm.Contacts.create({ name: 'Test', email: '[email protected]' });

  const call = (FMGofer.PerformScript as jest.Mock).mock.calls[0];
  const param = JSON.parse(call[1]);

  expect(param).toEqual({
    dapi: {
      action: 'create',
      layouts: 'Contacts',
      fieldData: { name: 'Test', email: '[email protected]' }
    }
  });
});

License

MIT

Contributing

Contributions welcome! Please submit issues and pull requests.

Support

For issues and questions:

  • GitHub Issues: [your-repo-url]
  • Documentation: This README

Changelog

1.0.0

  • Initial release
  • Full CRUD operations
  • Type-safe schema generation
  • Script execution support
  • React integration examples