@voyant-travel/custom-fields 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/api-runtime.d.ts +10 -0
  2. package/dist/api-runtime.js +13 -0
  3. package/dist/contracts.d.ts +81 -0
  4. package/dist/contracts.js +54 -0
  5. package/dist/contracts.test.d.ts +1 -0
  6. package/dist/contracts.test.js +34 -0
  7. package/dist/index.d.ts +9 -0
  8. package/dist/index.js +9 -0
  9. package/dist/registry.d.ts +8 -0
  10. package/dist/registry.js +61 -0
  11. package/dist/registry.test.d.ts +1 -0
  12. package/dist/registry.test.js +152 -0
  13. package/dist/routes.d.ts +15 -0
  14. package/dist/routes.js +210 -0
  15. package/dist/runtime-contributor.d.ts +10 -0
  16. package/dist/runtime-contributor.js +22 -0
  17. package/dist/runtime-contributor.test.d.ts +1 -0
  18. package/dist/runtime-contributor.test.js +30 -0
  19. package/dist/schema.d.ts +316 -0
  20. package/dist/schema.js +72 -0
  21. package/dist/service.d.ts +236 -0
  22. package/dist/service.js +304 -0
  23. package/dist/service.test.d.ts +1 -0
  24. package/dist/service.test.js +251 -0
  25. package/dist/target-capabilities.d.ts +12 -0
  26. package/dist/target-capabilities.js +11 -0
  27. package/dist/target-capabilities.test.d.ts +1 -0
  28. package/dist/target-capabilities.test.js +10 -0
  29. package/dist/targets.d.ts +10 -0
  30. package/dist/targets.js +17 -0
  31. package/dist/targets.test.d.ts +1 -0
  32. package/dist/targets.test.js +35 -0
  33. package/dist/value-contracts.d.ts +37 -0
  34. package/dist/value-contracts.js +36 -0
  35. package/dist/value-contracts.test.d.ts +1 -0
  36. package/dist/value-contracts.test.js +24 -0
  37. package/dist/value-mapping.d.ts +20 -0
  38. package/dist/value-mapping.js +68 -0
  39. package/dist/value-mapping.test.d.ts +1 -0
  40. package/dist/value-mapping.test.js +43 -0
  41. package/dist/value-service.d.ts +29 -0
  42. package/dist/value-service.js +208 -0
  43. package/dist/value-service.test.d.ts +1 -0
  44. package/dist/value-service.test.js +155 -0
  45. package/dist/voyant.d.ts +2 -0
  46. package/dist/voyant.js +104 -0
  47. package/migrations/0000_custom_fields_authority.sql +32 -0
  48. package/migrations/20260716000100_custom_field_namespace_ownership.sql +32 -0
  49. package/migrations/meta/_journal.json +20 -0
  50. package/openapi/admin/custom-fields.json +31 -0
  51. package/package.json +123 -0
@@ -0,0 +1,10 @@
1
+ import { createCustomFieldTargetRegistry } from "./targets.js";
2
+ interface CustomFieldsRuntimeContributorHost {
3
+ customFieldTargets?: Parameters<typeof createCustomFieldTargetRegistry>[0];
4
+ getRuntimePorts?<T>(port: {
5
+ id: string;
6
+ }): readonly T[] | Promise<readonly T[]>;
7
+ }
8
+ /** The generic package, not an entity module, provides database-backed definitions. */
9
+ export declare function createCustomFieldsRuntimePortContribution(host?: CustomFieldsRuntimeContributorHost): Readonly<Record<string, unknown>>;
10
+ export {};
@@ -0,0 +1,22 @@
1
+ import { customFieldsRuntimePort, customFieldValueReaderRuntimePort, } from "@voyant-travel/core/runtime-port";
2
+ import { loadCustomFieldRegistry, loadCustomFieldRegistryForWrite } from "./registry.js";
3
+ import { createCustomFieldTargetRegistry } from "./targets.js";
4
+ /** The generic package, not an entity module, provides database-backed definitions. */
5
+ export function createCustomFieldsRuntimePortContribution(host = {}) {
6
+ const targets = createCustomFieldTargetRegistry(host.customFieldTargets ?? []);
7
+ const runtime = {
8
+ resolveRegistry: (db) => loadCustomFieldRegistry(db, targets),
9
+ resolveRegistryForWrite: (db, entity) => loadCustomFieldRegistryForWrite(db, entity, targets),
10
+ async resolveVisibleValues(db, entity, entityId, channel) {
11
+ const readers = await Promise.resolve(host.getRuntimePorts?.(customFieldValueReaderRuntimePort) ??
12
+ []);
13
+ for (const reader of readers) {
14
+ const values = await reader.resolveVisibleValues(db, entity, entityId, channel);
15
+ if (values)
16
+ return values;
17
+ }
18
+ return {};
19
+ },
20
+ };
21
+ return { [customFieldsRuntimePort.id]: runtime };
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { customFieldsRuntimePort, customFieldValueReaderRuntimePort, } from "@voyant-travel/core/runtime-port";
2
+ import { describe, expect, it } from "vitest";
3
+ import { createCustomFieldsRuntimePortContribution } from "./runtime-contributor.js";
4
+ function runtimePortValues(values) {
5
+ return values;
6
+ }
7
+ describe("custom-fields runtime value readers", () => {
8
+ it("delegates visible values to selected entity readers", async () => {
9
+ const readers = [];
10
+ const reader = {
11
+ resolveVisibleValues: (_db, entity) => entity === "person" ? { custom: { loyalty: "gold" } } : undefined,
12
+ };
13
+ const runtime = createCustomFieldsRuntimePortContribution({
14
+ customFieldTargets: [
15
+ {
16
+ id: "person",
17
+ namespace: "relationships",
18
+ label: "Person",
19
+ fieldTypes: ["text"],
20
+ capabilities: ["read", "invoice"],
21
+ ownerUnitId: "@voyant-travel/relationships",
22
+ },
23
+ ],
24
+ getRuntimePorts: (port) => runtimePortValues(port.id === customFieldValueReaderRuntimePort.id ? readers : []),
25
+ })[customFieldsRuntimePort.id];
26
+ readers.push(reader);
27
+ await expect(runtime.resolveVisibleValues({}, "person", "person_1", "invoice")).resolves.toEqual({ custom: { loyalty: "gold" } });
28
+ await expect(runtime.resolveVisibleValues({}, "booking", "booking_1", "invoice")).resolves.toEqual({});
29
+ });
30
+ });
@@ -0,0 +1,316 @@
1
+ export declare const customFieldTypeEnum: import("drizzle-orm/pg-core").PgEnum<["varchar", "text", "double", "monetary", "date", "boolean", "enum", "set", "json", "address", "phone"]>;
2
+ export declare const customFieldOwnerKindEnum: import("drizzle-orm/pg-core").PgEnum<["platform", "operator", "app"]>;
3
+ export declare const customFieldLifecycleStateEnum: import("drizzle-orm/pg-core").PgEnum<["active", "inactive", "deprecated"]>;
4
+ /**
5
+ * The table was introduced by Relationships migration history. This generic
6
+ * package is its sole runtime/schema access owner from #3398 onward; it does
7
+ * not replay or mutate that immutable history.
8
+ */
9
+ export declare const customFieldDefinitions: import("drizzle-orm/pg-core").PgTableWithColumns<{
10
+ name: "custom_field_definitions";
11
+ schema: undefined;
12
+ columns: {
13
+ id: import("drizzle-orm/pg-core").PgColumn<{
14
+ name: string;
15
+ tableName: "custom_field_definitions";
16
+ dataType: "string";
17
+ columnType: "PgText";
18
+ data: string;
19
+ driverParam: string;
20
+ notNull: true;
21
+ hasDefault: true;
22
+ isPrimaryKey: true;
23
+ isAutoincrement: false;
24
+ hasRuntimeDefault: true;
25
+ enumValues: [string, ...string[]];
26
+ baseColumn: never;
27
+ identity: undefined;
28
+ generated: undefined;
29
+ }, {}, {}>;
30
+ entityType: import("drizzle-orm/pg-core").PgColumn<{
31
+ name: "entity_type";
32
+ tableName: "custom_field_definitions";
33
+ dataType: "string";
34
+ columnType: "PgText";
35
+ data: string;
36
+ driverParam: string;
37
+ notNull: true;
38
+ hasDefault: false;
39
+ isPrimaryKey: false;
40
+ isAutoincrement: false;
41
+ hasRuntimeDefault: false;
42
+ enumValues: [string, ...string[]];
43
+ baseColumn: never;
44
+ identity: undefined;
45
+ generated: undefined;
46
+ }, {}, {}>;
47
+ namespace: import("drizzle-orm/pg-core").PgColumn<{
48
+ name: "namespace";
49
+ tableName: "custom_field_definitions";
50
+ dataType: "string";
51
+ columnType: "PgText";
52
+ data: string;
53
+ driverParam: string;
54
+ notNull: true;
55
+ hasDefault: false;
56
+ isPrimaryKey: false;
57
+ isAutoincrement: false;
58
+ hasRuntimeDefault: false;
59
+ enumValues: [string, ...string[]];
60
+ baseColumn: never;
61
+ identity: undefined;
62
+ generated: undefined;
63
+ }, {}, {}>;
64
+ ownerKind: import("drizzle-orm/pg-core").PgColumn<{
65
+ name: "owner_kind";
66
+ tableName: "custom_field_definitions";
67
+ dataType: "string";
68
+ columnType: "PgEnumColumn";
69
+ data: "platform" | "operator" | "app";
70
+ driverParam: string;
71
+ notNull: true;
72
+ hasDefault: false;
73
+ isPrimaryKey: false;
74
+ isAutoincrement: false;
75
+ hasRuntimeDefault: false;
76
+ enumValues: ["platform", "operator", "app"];
77
+ baseColumn: never;
78
+ identity: undefined;
79
+ generated: undefined;
80
+ }, {}, {}>;
81
+ ownerId: import("drizzle-orm/pg-core").PgColumn<{
82
+ name: "owner_id";
83
+ tableName: "custom_field_definitions";
84
+ dataType: "string";
85
+ columnType: "PgText";
86
+ data: string;
87
+ driverParam: string;
88
+ notNull: false;
89
+ hasDefault: false;
90
+ isPrimaryKey: false;
91
+ isAutoincrement: false;
92
+ hasRuntimeDefault: false;
93
+ enumValues: [string, ...string[]];
94
+ baseColumn: never;
95
+ identity: undefined;
96
+ generated: undefined;
97
+ }, {}, {}>;
98
+ lifecycleState: import("drizzle-orm/pg-core").PgColumn<{
99
+ name: "lifecycle_state";
100
+ tableName: "custom_field_definitions";
101
+ dataType: "string";
102
+ columnType: "PgEnumColumn";
103
+ data: "active" | "inactive" | "deprecated";
104
+ driverParam: string;
105
+ notNull: true;
106
+ hasDefault: false;
107
+ isPrimaryKey: false;
108
+ isAutoincrement: false;
109
+ hasRuntimeDefault: false;
110
+ enumValues: ["active", "inactive", "deprecated"];
111
+ baseColumn: never;
112
+ identity: undefined;
113
+ generated: undefined;
114
+ }, {}, {}>;
115
+ provenance: import("drizzle-orm/pg-core").PgColumn<{
116
+ name: "provenance";
117
+ tableName: "custom_field_definitions";
118
+ dataType: "json";
119
+ columnType: "PgJsonb";
120
+ data: Record<string, unknown>;
121
+ driverParam: unknown;
122
+ notNull: true;
123
+ hasDefault: false;
124
+ isPrimaryKey: false;
125
+ isAutoincrement: false;
126
+ hasRuntimeDefault: false;
127
+ enumValues: undefined;
128
+ baseColumn: never;
129
+ identity: undefined;
130
+ generated: undefined;
131
+ }, {}, {
132
+ $type: Record<string, unknown>;
133
+ }>;
134
+ key: import("drizzle-orm/pg-core").PgColumn<{
135
+ name: "key";
136
+ tableName: "custom_field_definitions";
137
+ dataType: "string";
138
+ columnType: "PgText";
139
+ data: string;
140
+ driverParam: string;
141
+ notNull: true;
142
+ hasDefault: false;
143
+ isPrimaryKey: false;
144
+ isAutoincrement: false;
145
+ hasRuntimeDefault: false;
146
+ enumValues: [string, ...string[]];
147
+ baseColumn: never;
148
+ identity: undefined;
149
+ generated: undefined;
150
+ }, {}, {}>;
151
+ label: import("drizzle-orm/pg-core").PgColumn<{
152
+ name: "label";
153
+ tableName: "custom_field_definitions";
154
+ dataType: "string";
155
+ columnType: "PgText";
156
+ data: string;
157
+ driverParam: string;
158
+ notNull: true;
159
+ hasDefault: false;
160
+ isPrimaryKey: false;
161
+ isAutoincrement: false;
162
+ hasRuntimeDefault: false;
163
+ enumValues: [string, ...string[]];
164
+ baseColumn: never;
165
+ identity: undefined;
166
+ generated: undefined;
167
+ }, {}, {}>;
168
+ fieldType: import("drizzle-orm/pg-core").PgColumn<{
169
+ name: "field_type";
170
+ tableName: "custom_field_definitions";
171
+ dataType: "string";
172
+ columnType: "PgEnumColumn";
173
+ data: "boolean" | "text" | "date" | "monetary" | "json" | "varchar" | "double" | "enum" | "set" | "address" | "phone";
174
+ driverParam: string;
175
+ notNull: true;
176
+ hasDefault: false;
177
+ isPrimaryKey: false;
178
+ isAutoincrement: false;
179
+ hasRuntimeDefault: false;
180
+ enumValues: ["varchar", "text", "double", "monetary", "date", "boolean", "enum", "set", "json", "address", "phone"];
181
+ baseColumn: never;
182
+ identity: undefined;
183
+ generated: undefined;
184
+ }, {}, {}>;
185
+ isRequired: import("drizzle-orm/pg-core").PgColumn<{
186
+ name: "is_required";
187
+ tableName: "custom_field_definitions";
188
+ dataType: "boolean";
189
+ columnType: "PgBoolean";
190
+ data: boolean;
191
+ driverParam: boolean;
192
+ notNull: true;
193
+ hasDefault: true;
194
+ isPrimaryKey: false;
195
+ isAutoincrement: false;
196
+ hasRuntimeDefault: false;
197
+ enumValues: undefined;
198
+ baseColumn: never;
199
+ identity: undefined;
200
+ generated: undefined;
201
+ }, {}, {}>;
202
+ isSearchable: import("drizzle-orm/pg-core").PgColumn<{
203
+ name: "is_searchable";
204
+ tableName: "custom_field_definitions";
205
+ dataType: "boolean";
206
+ columnType: "PgBoolean";
207
+ data: boolean;
208
+ driverParam: boolean;
209
+ notNull: true;
210
+ hasDefault: true;
211
+ isPrimaryKey: false;
212
+ isAutoincrement: false;
213
+ hasRuntimeDefault: false;
214
+ enumValues: undefined;
215
+ baseColumn: never;
216
+ identity: undefined;
217
+ generated: undefined;
218
+ }, {}, {}>;
219
+ isExportable: import("drizzle-orm/pg-core").PgColumn<{
220
+ name: "is_exportable";
221
+ tableName: "custom_field_definitions";
222
+ dataType: "boolean";
223
+ columnType: "PgBoolean";
224
+ data: boolean;
225
+ driverParam: boolean;
226
+ notNull: true;
227
+ hasDefault: true;
228
+ isPrimaryKey: false;
229
+ isAutoincrement: false;
230
+ hasRuntimeDefault: false;
231
+ enumValues: undefined;
232
+ baseColumn: never;
233
+ identity: undefined;
234
+ generated: undefined;
235
+ }, {}, {}>;
236
+ isInvoiceable: import("drizzle-orm/pg-core").PgColumn<{
237
+ name: "is_invoiceable";
238
+ tableName: "custom_field_definitions";
239
+ dataType: "boolean";
240
+ columnType: "PgBoolean";
241
+ data: boolean;
242
+ driverParam: boolean;
243
+ notNull: true;
244
+ hasDefault: true;
245
+ isPrimaryKey: false;
246
+ isAutoincrement: false;
247
+ hasRuntimeDefault: false;
248
+ enumValues: undefined;
249
+ baseColumn: never;
250
+ identity: undefined;
251
+ generated: undefined;
252
+ }, {}, {}>;
253
+ options: import("drizzle-orm/pg-core").PgColumn<{
254
+ name: "options";
255
+ tableName: "custom_field_definitions";
256
+ dataType: "json";
257
+ columnType: "PgJsonb";
258
+ data: {
259
+ label: string;
260
+ value: string;
261
+ }[];
262
+ driverParam: unknown;
263
+ notNull: false;
264
+ hasDefault: false;
265
+ isPrimaryKey: false;
266
+ isAutoincrement: false;
267
+ hasRuntimeDefault: false;
268
+ enumValues: undefined;
269
+ baseColumn: never;
270
+ identity: undefined;
271
+ generated: undefined;
272
+ }, {}, {
273
+ $type: {
274
+ label: string;
275
+ value: string;
276
+ }[];
277
+ }>;
278
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
279
+ name: "created_at";
280
+ tableName: "custom_field_definitions";
281
+ dataType: "date";
282
+ columnType: "PgTimestamp";
283
+ data: Date;
284
+ driverParam: string;
285
+ notNull: true;
286
+ hasDefault: true;
287
+ isPrimaryKey: false;
288
+ isAutoincrement: false;
289
+ hasRuntimeDefault: false;
290
+ enumValues: undefined;
291
+ baseColumn: never;
292
+ identity: undefined;
293
+ generated: undefined;
294
+ }, {}, {}>;
295
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
296
+ name: "updated_at";
297
+ tableName: "custom_field_definitions";
298
+ dataType: "date";
299
+ columnType: "PgTimestamp";
300
+ data: Date;
301
+ driverParam: string;
302
+ notNull: true;
303
+ hasDefault: true;
304
+ isPrimaryKey: false;
305
+ isAutoincrement: false;
306
+ hasRuntimeDefault: false;
307
+ enumValues: undefined;
308
+ baseColumn: never;
309
+ identity: undefined;
310
+ generated: undefined;
311
+ }, {}, {}>;
312
+ };
313
+ dialect: "pg";
314
+ }>;
315
+ export type CustomFieldDefinition = typeof customFieldDefinitions.$inferSelect;
316
+ export type NewCustomFieldDefinition = typeof customFieldDefinitions.$inferInsert;
package/dist/schema.js ADDED
@@ -0,0 +1,72 @@
1
+ import { typeId } from "@voyant-travel/db/lib/typeid-column";
2
+ import { sql } from "drizzle-orm";
3
+ import { boolean, check, index, jsonb, pgEnum, pgTable, text, timestamp, uniqueIndex, } from "drizzle-orm/pg-core";
4
+ export const customFieldTypeEnum = pgEnum("custom_field_type", [
5
+ "varchar",
6
+ "text",
7
+ "double",
8
+ "monetary",
9
+ "date",
10
+ "boolean",
11
+ "enum",
12
+ "set",
13
+ "json",
14
+ "address",
15
+ "phone",
16
+ ]);
17
+ export const customFieldOwnerKindEnum = pgEnum("custom_field_owner_kind", [
18
+ "platform",
19
+ "operator",
20
+ "app",
21
+ ]);
22
+ export const customFieldLifecycleStateEnum = pgEnum("custom_field_lifecycle_state", [
23
+ "active",
24
+ "inactive",
25
+ "deprecated",
26
+ ]);
27
+ /**
28
+ * The table was introduced by Relationships migration history. This generic
29
+ * package is its sole runtime/schema access owner from #3398 onward; it does
30
+ * not replay or mutate that immutable history.
31
+ */
32
+ export const customFieldDefinitions = pgTable("custom_field_definitions", {
33
+ id: typeId("custom_field_definitions"),
34
+ entityType: text("entity_type").notNull(),
35
+ namespace: text("namespace").notNull(),
36
+ ownerKind: customFieldOwnerKindEnum("owner_kind").notNull(),
37
+ ownerId: text("owner_id"),
38
+ lifecycleState: customFieldLifecycleStateEnum("lifecycle_state").notNull(),
39
+ provenance: jsonb("provenance").$type().notNull(),
40
+ key: text("key").notNull(),
41
+ label: text("label").notNull(),
42
+ fieldType: customFieldTypeEnum("field_type").notNull(),
43
+ isRequired: boolean("is_required").notNull().default(false),
44
+ isSearchable: boolean("is_searchable").notNull().default(false),
45
+ isExportable: boolean("is_exportable").notNull().default(true),
46
+ isInvoiceable: boolean("is_invoiceable").notNull().default(false),
47
+ options: jsonb("options").$type(),
48
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
49
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
50
+ }, (table) => [
51
+ index("idx_custom_field_definitions_entity").on(table.entityType),
52
+ index("idx_custom_field_definitions_entity_label").on(table.entityType, table.label),
53
+ index("idx_custom_field_definitions_owner").on(table.ownerKind, table.ownerId, table.lifecycleState),
54
+ index("idx_custom_field_definitions_namespace").on(table.namespace, table.entityType),
55
+ check("custom_field_definitions_owner_identity",
56
+ // agent-quality: raw-sql reviewed -- owner: custom-fields; identifiers are Drizzle-owned columns and all literals are static.
57
+ sql `(
58
+ (${table.ownerKind} = 'operator' AND ${table.ownerId} IS NULL AND ${table.namespace} = 'custom')
59
+ OR (
60
+ ${table.ownerKind} = 'platform'
61
+ AND ${table.ownerId} IS NOT NULL
62
+ AND ${table.namespace} <> 'custom'
63
+ AND ${table.namespace} NOT LIKE 'app--%'
64
+ )
65
+ OR (
66
+ ${table.ownerKind} = 'app'
67
+ AND ${table.ownerId} IS NOT NULL
68
+ AND ${table.namespace} LIKE 'app--%'
69
+ )
70
+ )`),
71
+ uniqueIndex("uidx_custom_field_definitions_namespace_key").on(table.entityType, table.namespace, table.key),
72
+ ]);