Skip to content

Commit c09d7bb

Browse files
committed
specialcolumns header
1 parent 5824811 commit c09d7bb

17 files changed

+1227
-110
lines changed

packages/fmodata/README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ const result = await db.webhook.add({
808808
tableName: contactsTable,
809809
headers: {
810810
"X-Custom-Header": "value",
811-
"Authorization": "Bearer token",
811+
Authorization: "Bearer token",
812812
},
813813
notifySchemaChanges: true, // Notify when schema changes
814814
});
@@ -1442,6 +1442,37 @@ const users = fmTableOccurrence(
14421442
);
14431443
```
14441444

1445+
### Special Columns (ROWID and ROWMODID)
1446+
1447+
FileMaker provides special columns `ROWID` and `ROWMODID` that uniquely identify records and track modifications. These can be included in query responses when enabled.
1448+
1449+
Enable special columns at the database level:
1450+
1451+
```typescript
1452+
const db = connection.database("MyDatabase", {
1453+
includeSpecialColumns: true,
1454+
});
1455+
1456+
const result = await db.from(users).list().execute();
1457+
// result.data[0] will have ROWID and ROWMODID properties
1458+
```
1459+
1460+
Override at the request level:
1461+
1462+
```typescript
1463+
// Enable for this request only
1464+
const result = await db.from(users).list().execute({
1465+
includeSpecialColumns: true,
1466+
});
1467+
1468+
// Disable for this request
1469+
const result = await db.from(users).list().execute({
1470+
includeSpecialColumns: false,
1471+
});
1472+
```
1473+
1474+
**Important:** Special columns are only included when no `$select` query is applied (per OData specification). When using `.select()`, special columns are excluded even if `includeSpecialColumns` is enabled.
1475+
14451476
### Error Handling
14461477

14471478
All operations return a `Result` type with either `data` or `error`. The library provides rich error types that help you handle different error scenarios appropriately.

packages/fmodata/src/client/builders/default-select.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ function getContainerFieldNames(table: FMTable<any, any>): string[] {
2020
* Gets default select fields from a table definition.
2121
* Returns undefined if defaultSelect is "all".
2222
* Automatically filters out container fields since they cannot be selected via $select.
23+
*
24+
* @param table - The table occurrence
25+
* @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is "schema"
2326
*/
2427
export function getDefaultSelectFields(
2528
table: FMTable<any, any> | undefined,
29+
includeSpecialColumns?: boolean,
2630
): string[] | undefined {
2731
if (!table) return undefined;
2832

@@ -33,7 +37,14 @@ export function getDefaultSelectFields(
3337
const baseTableConfig = getBaseTableConfig(table);
3438
const allFields = Object.keys(baseTableConfig.schema);
3539
// Filter out container fields
36-
return [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
40+
const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
41+
42+
// Add special columns if requested
43+
if (includeSpecialColumns) {
44+
fields.push("ROWID", "ROWMODID");
45+
}
46+
47+
return fields;
3748
}
3849

3950
if (Array.isArray(defaultSelect)) {

packages/fmodata/src/client/builders/query-string-builder.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,26 @@ export function buildSelectExpandQueryString(config: {
1717
table?: FMTable<any, any>;
1818
useEntityIds: boolean;
1919
logger: InternalLogger;
20+
includeSpecialColumns?: boolean;
2021
}): string {
2122
const parts: string[] = [];
2223
const expandBuilder = new ExpandBuilder(config.useEntityIds, config.logger);
2324

2425
// Build $select
2526
if (config.selectedFields && config.selectedFields.length > 0) {
27+
// Add special columns if includeSpecialColumns is true and they're not already present
28+
let finalSelectedFields = [...config.selectedFields];
29+
if (config.includeSpecialColumns) {
30+
if (!finalSelectedFields.includes("ROWID")) {
31+
finalSelectedFields.push("ROWID");
32+
}
33+
if (!finalSelectedFields.includes("ROWMODID")) {
34+
finalSelectedFields.push("ROWMODID");
35+
}
36+
}
37+
2638
const selectString = formatSelectFields(
27-
config.selectedFields,
39+
finalSelectedFields,
2840
config.table,
2941
config.useEntityIds,
3042
);

packages/fmodata/src/client/builders/response-processor.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface ProcessResponseConfig {
1717
expandValidationConfigs?: ExpandValidationConfig[];
1818
skipValidation?: boolean;
1919
useEntityIds?: boolean;
20+
includeSpecialColumns?: boolean;
2021
// Mapping from field names to output keys (for renamed fields in select)
2122
fieldMapping?: Record<string, string>;
2223
}
@@ -37,6 +38,7 @@ export async function processODataResponse<T>(
3738
expandValidationConfigs,
3839
skipValidation,
3940
useEntityIds,
41+
includeSpecialColumns,
4042
fieldMapping,
4143
} = config;
4244

@@ -67,13 +69,17 @@ export async function processODataResponse<T>(
6769
}
6870

6971
// Validation path
72+
// Note: Special columns are excluded when using QueryBuilder.single() method,
73+
// but included for RecordBuilder.get() method (both use singleMode: "exact")
74+
// The exclusion is handled in QueryBuilder's processQueryResponse, not here
7075
if (singleMode !== false) {
7176
const validation = await validateSingleResponse<any>(
7277
response,
7378
schema,
7479
selectedFields as any,
7580
expandValidationConfigs,
7681
singleMode,
82+
includeSpecialColumns,
7783
);
7884

7985
if (!validation.valid) {
@@ -96,6 +102,7 @@ export async function processODataResponse<T>(
96102
schema,
97103
selectedFields as any,
98104
expandValidationConfigs,
105+
includeSpecialColumns,
99106
);
100107

101108
if (!validation.valid) {
@@ -223,6 +230,7 @@ export async function processQueryResponse<T>(
223230
expandConfigs: ExpandConfig[];
224231
skipValidation?: boolean;
225232
useEntityIds?: boolean;
233+
includeSpecialColumns?: boolean;
226234
// Mapping from field names to output keys (for renamed fields in select)
227235
fieldMapping?: Record<string, string>;
228236
logger: InternalLogger;
@@ -235,6 +243,7 @@ export async function processQueryResponse<T>(
235243
expandConfigs,
236244
skipValidation,
237245
useEntityIds,
246+
includeSpecialColumns,
238247
fieldMapping,
239248
logger,
240249
} = config;
@@ -258,6 +267,7 @@ export async function processQueryResponse<T>(
258267
expandValidationConfigs,
259268
skipValidation,
260269
useEntityIds,
270+
includeSpecialColumns,
261271
});
262272

263273
// Rename fields if field mapping is provided (for renamed fields in select)

packages/fmodata/src/client/database.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { SchemaManager } from "./schema-manager";
66
import { FMTable } from "../orm/table";
77
import { WebhookManager } from "./webhook-builder";
88

9-
export class Database {
9+
export class Database<IncludeSpecialColumns extends boolean = false> {
1010
private _useEntityIds: boolean = false;
11+
private _includeSpecialColumns: IncludeSpecialColumns;
1112
public readonly schema: SchemaManager;
1213
public readonly webhook: WebhookManager;
1314

@@ -21,15 +22,24 @@ export class Database {
2122
* If set to false but some occurrences do not use entity IDs, an error will be thrown
2223
*/
2324
useEntityIds?: boolean;
25+
/**
26+
* Whether to include special columns (ROWID and ROWMODID) in responses.
27+
* Note: Special columns are only included when there is no $select query.
28+
*/
29+
includeSpecialColumns?: IncludeSpecialColumns;
2430
},
2531
) {
2632
// Initialize schema manager
2733
this.schema = new SchemaManager(this.databaseName, this.context);
2834
this.webhook = new WebhookManager(this.databaseName, this.context);
2935
this._useEntityIds = config?.useEntityIds ?? false;
36+
this._includeSpecialColumns = (config?.includeSpecialColumns ??
37+
false) as IncludeSpecialColumns;
3038
}
3139

32-
from<T extends FMTable<any, any>>(table: T): EntitySet<T> {
40+
from<T extends FMTable<any, any>>(
41+
table: T,
42+
): EntitySet<T, IncludeSpecialColumns> {
3343
// Only override database-level useEntityIds if table explicitly sets it
3444
// (not if it's undefined, which would override the database setting)
3545
if (
@@ -40,7 +50,7 @@ export class Database {
4050
this._useEntityIds = tableUseEntityIds;
4151
}
4252
}
43-
return new EntitySet<T>({
53+
return new EntitySet<T, IncludeSpecialColumns>({
4454
occurrence: table as T,
4555
databaseName: this.databaseName,
4656
context: this.context,

packages/fmodata/src/client/delete-builder.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {
22
ExecutionContext,
33
ExecutableBuilder,
44
Result,
5-
WithSystemFields,
5+
WithSpecialColumns,
66
ExecuteOptions,
77
ExecuteMethodOptions,
88
} from "../types";
@@ -26,17 +26,21 @@ export class DeleteBuilder<Occ extends FMTable<any, any>> {
2626
private context: ExecutionContext;
2727
private table: Occ;
2828
private databaseUseEntityIds: boolean;
29+
private databaseIncludeSpecialColumns: boolean;
2930

3031
constructor(config: {
3132
occurrence: Occ;
3233
databaseName: string;
3334
context: ExecutionContext;
3435
databaseUseEntityIds?: boolean;
36+
databaseIncludeSpecialColumns?: boolean;
3537
}) {
3638
this.table = config.occurrence;
3739
this.databaseName = config.databaseName;
3840
this.context = config.context;
3941
this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
42+
this.databaseIncludeSpecialColumns =
43+
config.databaseIncludeSpecialColumns ?? false;
4044
}
4145

4246
/**

0 commit comments

Comments
 (0)