fm-jsdriver
v1.0.2
Published
Type-safe FileMaker driver for webviewer communication using fm-gofer
Maintainers
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
- Communication is ONLY through FileMaker webviewer (no HTTP/REST API)
- Uses fm-gofer package for promise-based FileMaker script execution
- All operations use FileMaker's internal recordId as handles (not primary keys)
- Primary keys are auto-generated by FileMaker and returned in responses
- All methods communicate via
FMGofer.PerformScript('jsDriver', JSON.stringify(parameter))
Installation
npm install fm-jsdriverQuick 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.tsThis 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 fieldsnumber- Numeric fieldsboolean- Boolean fieldsdate- Date fieldstime- Time fieldstimestamp- 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 recordprescript?: ScriptInput- Optional script to run before creationscript?: 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 IDfieldData: Partial<T>- Fields to update (partial)prescript?: ScriptInput- Optional script to run before updatescript?: 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 IDprescript?: ScriptInput- Optional script to run before retrievalscript?: 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 criteriaoptions?: FindOptions- Pagination optionsoffset?: number- Starting record (default: 1)limit?: number- Maximum records (default: 100)
prescript?: ScriptInput- Optional script to run before findscript?: 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 optionsprescript?: ScriptInput- Optional script to run before listscript?: 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 IDprescript?: ScriptInput- Optional script to run before deletionscript?: 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:
- Identify Layouts: Look at
fmSchema.layoutskeys for layout names - Identify Fields: For each layout, examine
fieldsfor available field names - Identify Primary Keys: Fields with
_meta.autoEnter: trueand_meta.notEmpty: trueare primary keys - Identify Required Fields: Fields with
_meta.notEmpty: true(but not autoEnter) are required - Identify Field Types: Use the
typeproperty 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
- Always use recordId: Never use primary key fields for get/update/delete - only recordId
- Include required fields: Check schema for
_meta.notEmpty: truefields - Use Partial for updates: Updates should use
Partial<T>type - 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
- Type-safe field names: Only use field names that exist in the schema
- Handle async: All operations return Promises - use async/await
- Error handling: Wrap operations in try/catch for production code
- Boolean fields: FileMaker doesn't support boolean fields. Instead of boolean fields of true/false, create a number field of 1/0
- 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
- 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
- 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
