@eide/foir-cli 0.1.3 → 0.1.5

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 (40) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/codegen/fetch-customer-profile-schema.d.ts +12 -0
  3. package/dist/codegen/fetch-customer-profile-schema.d.ts.map +1 -0
  4. package/dist/codegen/fetch-customer-profile-schema.js +21 -0
  5. package/dist/codegen/fetch-models.d.ts.map +1 -1
  6. package/dist/codegen/field-mapping.d.ts.map +1 -1
  7. package/dist/codegen/field-mapping.js +60 -16
  8. package/dist/codegen/generators/customer-profile-types.d.ts +6 -0
  9. package/dist/codegen/generators/customer-profile-types.d.ts.map +1 -0
  10. package/dist/codegen/generators/customer-profile-types.js +45 -0
  11. package/dist/codegen/generators/model-types.d.ts.map +1 -1
  12. package/dist/codegen/generators/model-types.js +4 -2
  13. package/dist/codegen/generators/swift-field-types.d.ts +5 -0
  14. package/dist/codegen/generators/swift-field-types.d.ts.map +1 -0
  15. package/dist/codegen/generators/swift-field-types.js +145 -0
  16. package/dist/codegen/generators/swift-model-keys.d.ts +6 -0
  17. package/dist/codegen/generators/swift-model-keys.d.ts.map +1 -0
  18. package/dist/codegen/generators/swift-model-keys.js +25 -0
  19. package/dist/codegen/generators/swift-types.d.ts +13 -0
  20. package/dist/codegen/generators/swift-types.d.ts.map +1 -0
  21. package/dist/codegen/generators/swift-types.js +186 -0
  22. package/dist/codegen/swift-field-mapping.d.ts +30 -0
  23. package/dist/codegen/swift-field-mapping.d.ts.map +1 -0
  24. package/dist/codegen/swift-field-mapping.js +156 -0
  25. package/dist/codegen/write-files.d.ts.map +1 -1
  26. package/dist/codegen/write-files.js +2 -1
  27. package/dist/commands/customer-profiles.d.ts +4 -0
  28. package/dist/commands/customer-profiles.d.ts.map +1 -0
  29. package/dist/commands/customer-profiles.js +97 -0
  30. package/dist/commands/pull.d.ts.map +1 -1
  31. package/dist/commands/pull.js +61 -11
  32. package/dist/config/pull-config.d.ts +2 -0
  33. package/dist/config/pull-config.d.ts.map +1 -1
  34. package/dist/config/pull-config.js +7 -2
  35. package/dist/config/types.d.ts +2 -0
  36. package/dist/config/types.d.ts.map +1 -1
  37. package/dist/graphql/queries.d.ts +4 -0
  38. package/dist/graphql/queries.d.ts.map +1 -1
  39. package/dist/graphql/queries.js +13 -0
  40. package/package.json +11 -11
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Generates per-model Swift files with:
3
+ * - Field key enum (compile-time safe field keys)
4
+ * - Data struct (typed representation)
5
+ * - Serialization helpers (toSyncData / fromSyncData)
6
+ * - Model config constant
7
+ */
8
+ import { toPascalCase } from '../field-mapping.js';
9
+ import { getSwiftFieldType, SWIFT_FIELD_TYPE_MAPPING, } from '../swift-field-mapping.js';
10
+ /**
11
+ * Generate a complete Swift file for a single model.
12
+ */
13
+ export function generateSwiftModelFile(model) {
14
+ const typeName = toPascalCase(model.key);
15
+ const fields = model.fields ?? [];
16
+ const lines = [];
17
+ // Header
18
+ lines.push('//');
19
+ lines.push(`// ${typeName}.swift`);
20
+ lines.push('//');
21
+ lines.push(`// Generated from model '${model.key}'`);
22
+ lines.push('//');
23
+ lines.push('// @generated by foir \u2014 DO NOT EDIT MANUALLY');
24
+ lines.push('//');
25
+ lines.push('');
26
+ lines.push('import Foundation');
27
+ lines.push('');
28
+ // 1. Field key enum
29
+ lines.push(generateFieldsEnum(typeName, fields));
30
+ lines.push('');
31
+ // 2. Data struct
32
+ lines.push(generateDataStruct(typeName, fields));
33
+ lines.push('');
34
+ // 3. Serialization extension
35
+ lines.push(generateSerializationExtension(typeName, fields));
36
+ lines.push('');
37
+ // 4. Config enum
38
+ lines.push(generateConfigEnum(typeName, model));
39
+ return lines.join('\n');
40
+ }
41
+ function generateFieldsEnum(typeName, fields) {
42
+ const lines = [];
43
+ lines.push(`// MARK: - ${typeName} Field Keys`);
44
+ lines.push('');
45
+ lines.push(`enum ${typeName}Fields {`);
46
+ for (const field of fields) {
47
+ lines.push(` static let ${field.key} = "${field.key}"`);
48
+ }
49
+ lines.push('}');
50
+ return lines.join('\n');
51
+ }
52
+ function generateDataStruct(typeName, fields) {
53
+ const lines = [];
54
+ lines.push(`// MARK: - ${typeName} Data`);
55
+ lines.push('');
56
+ lines.push(`struct ${typeName}Data {`);
57
+ for (const field of fields) {
58
+ const { type, isOptional } = getSwiftFieldType(field);
59
+ const optionalSuffix = isOptional ? '?' : '';
60
+ lines.push(` var ${field.key}: ${type}${optionalSuffix}`);
61
+ }
62
+ lines.push('}');
63
+ return lines.join('\n');
64
+ }
65
+ function generateSerializationExtension(typeName, fields) {
66
+ const lines = [];
67
+ lines.push(`// MARK: - ${typeName} Serialization`);
68
+ lines.push('');
69
+ lines.push(`extension ${typeName}Data {`);
70
+ // toSyncData()
71
+ lines.push(' func toSyncData() -> [String: Any] {');
72
+ // Find the first required (non-optional) field to seed the dictionary
73
+ const requiredFields = fields.filter((f) => {
74
+ const { isOptional } = getSwiftFieldType(f);
75
+ return !isOptional;
76
+ });
77
+ const optionalFields = fields.filter((f) => {
78
+ const { isOptional } = getSwiftFieldType(f);
79
+ return isOptional;
80
+ });
81
+ if (requiredFields.length > 0) {
82
+ if (optionalFields.length === 0) {
83
+ // All fields are required — return inline
84
+ lines.push(` return [`);
85
+ requiredFields.forEach((f, i) => {
86
+ const comma = i < requiredFields.length - 1 ? ',' : '';
87
+ lines.push(` ${typeName}Fields.${f.key}: ${toSyncValueExpr(f)}${comma}`);
88
+ });
89
+ lines.push(' ]');
90
+ }
91
+ else {
92
+ lines.push(` var data: [String: Any] = [`);
93
+ requiredFields.forEach((f, i) => {
94
+ const comma = i < requiredFields.length - 1 ? ',' : '';
95
+ lines.push(` ${typeName}Fields.${f.key}: ${toSyncValueExpr(f)}${comma}`);
96
+ });
97
+ lines.push(' ]');
98
+ for (const f of optionalFields) {
99
+ lines.push(` if let ${f.key} { data[${typeName}Fields.${f.key}] = ${toSyncValueExprForOptional(f)} }`);
100
+ }
101
+ lines.push(' return data');
102
+ }
103
+ }
104
+ else {
105
+ // All fields are optional
106
+ lines.push(' var data: [String: Any] = [:]');
107
+ for (const f of optionalFields) {
108
+ lines.push(` if let ${f.key} { data[${typeName}Fields.${f.key}] = ${toSyncValueExprForOptional(f)} }`);
109
+ }
110
+ lines.push(' return data');
111
+ }
112
+ lines.push(' }');
113
+ lines.push('');
114
+ // fromSyncData()
115
+ lines.push(' static func fromSyncData(_ data: [String: Any]) -> ' +
116
+ typeName +
117
+ 'Data {');
118
+ lines.push(` ${typeName}Data(`);
119
+ fields.forEach((field, i) => {
120
+ const comma = i < fields.length - 1 ? ',' : '';
121
+ const { isOptional, mapping } = getSwiftFieldType(field);
122
+ lines.push(` ${field.key}: ${fromSyncDataExpr(field, typeName, isOptional, mapping)}${comma}`);
123
+ });
124
+ lines.push(' )');
125
+ lines.push(' }');
126
+ lines.push('}');
127
+ return lines.join('\n');
128
+ }
129
+ /** Expression for a required field value in toSyncData */
130
+ function toSyncValueExpr(field) {
131
+ const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
132
+ if (mapping?.needsSharedType) {
133
+ return `${field.key}.toSyncData()`;
134
+ }
135
+ return field.key;
136
+ }
137
+ /** Expression for an optional field value in toSyncData (inside `if let`) */
138
+ function toSyncValueExprForOptional(field) {
139
+ const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
140
+ if (mapping?.needsSharedType) {
141
+ return `${field.key}.toSyncData()`;
142
+ }
143
+ return field.key;
144
+ }
145
+ /** Expression for reading a field from sync data dictionary */
146
+ function fromSyncDataExpr(field, typeName, isOptional, mapping) {
147
+ const accessor = `data[${typeName}Fields.${field.key}]`;
148
+ if (!mapping) {
149
+ // Unknown type
150
+ return isOptional ? `${accessor}` : `${accessor} ?? nil`;
151
+ }
152
+ // Types with shared structs need .fromSyncData conversion
153
+ if (mapping.needsSharedType) {
154
+ const dictCast = `${accessor} as? [String: Any]`;
155
+ if (isOptional) {
156
+ return `(${dictCast}).map { ${mapping.type}.fromSyncData($0) }`;
157
+ }
158
+ return `${mapping.type}.fromSyncData(${dictCast} ?? [:])`;
159
+ }
160
+ // json → Any
161
+ if (field.type === 'json') {
162
+ return isOptional ? accessor : `${accessor}`;
163
+ }
164
+ // Simple cast types
165
+ if (isOptional) {
166
+ return `${accessor} ${mapping.castExpression}`;
167
+ }
168
+ return `${accessor} ${mapping.castExpression} ?? ${mapping.defaultValue}`;
169
+ }
170
+ function generateConfigEnum(typeName, model) {
171
+ const lines = [];
172
+ lines.push(`// MARK: - ${typeName} Config`);
173
+ lines.push('');
174
+ lines.push(`enum ${typeName}Config {`);
175
+ const escapedName = (model.name ?? model.key).replace(/"/g, '\\"');
176
+ lines.push(` static let key = "${model.key}"`);
177
+ lines.push(` static let name = "${escapedName}"`);
178
+ lines.push(` static let customerScoped = ${model.config.customerScoped}`);
179
+ lines.push(` static let publicApi = ${model.config.publicApi}`);
180
+ lines.push(` static let versioning = ${model.config.versioning}`);
181
+ lines.push(` static let publishing = ${model.config.publishing}`);
182
+ lines.push(` static let variants = ${model.config.variants}`);
183
+ lines.push('}');
184
+ lines.push('');
185
+ return lines.join('\n');
186
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Maps EIDE field types to Swift types for sync data serialization.
3
+ *
4
+ * Reference fields map to String (not a struct) because sync data
5
+ * payloads carry plain string IDs — the platform resolves references separately.
6
+ */
7
+ import type { FieldSchemaForGen } from './field-mapping.js';
8
+ export interface SwiftTypeMapping {
9
+ /** Swift type name */
10
+ type: string;
11
+ /** Whether the field is always optional (regardless of required flag) */
12
+ alwaysOptional: boolean;
13
+ /** Default value expression for non-optional required fields in fromSyncData */
14
+ defaultValue: string;
15
+ /** The `as?` cast expression for fromSyncData */
16
+ castExpression: string;
17
+ /** Whether this type needs a shared struct in FieldTypes.swift */
18
+ needsSharedType?: boolean;
19
+ }
20
+ export declare const SWIFT_FIELD_TYPE_MAPPING: Record<string, SwiftTypeMapping>;
21
+ /**
22
+ * Get the Swift type for a field.
23
+ * Returns { type, isOptional } so callers can decide on `?` suffix.
24
+ */
25
+ export declare function getSwiftFieldType(field: FieldSchemaForGen): {
26
+ type: string;
27
+ isOptional: boolean;
28
+ mapping: SwiftTypeMapping | undefined;
29
+ };
30
+ //# sourceMappingURL=swift-field-mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swift-field-mapping.d.ts","sourceRoot":"","sources":["../../src/codegen/swift-field-mapping.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;IACxB,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAoIrE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAAC;CACvC,CAeA"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Maps EIDE field types to Swift types for sync data serialization.
3
+ *
4
+ * Reference fields map to String (not a struct) because sync data
5
+ * payloads carry plain string IDs — the platform resolves references separately.
6
+ */
7
+ export const SWIFT_FIELD_TYPE_MAPPING = {
8
+ text: {
9
+ type: 'String',
10
+ alwaysOptional: false,
11
+ defaultValue: '""',
12
+ castExpression: 'as? String',
13
+ },
14
+ richtext: {
15
+ type: 'String',
16
+ alwaysOptional: true,
17
+ defaultValue: '""',
18
+ castExpression: 'as? String',
19
+ },
20
+ number: {
21
+ type: 'Double',
22
+ alwaysOptional: true,
23
+ defaultValue: '0',
24
+ castExpression: 'as? Double',
25
+ },
26
+ boolean: {
27
+ type: 'Bool',
28
+ alwaysOptional: true,
29
+ defaultValue: 'false',
30
+ castExpression: 'as? Bool',
31
+ },
32
+ email: {
33
+ type: 'String',
34
+ alwaysOptional: true,
35
+ defaultValue: '""',
36
+ castExpression: 'as? String',
37
+ },
38
+ phone: {
39
+ type: 'String',
40
+ alwaysOptional: true,
41
+ defaultValue: '""',
42
+ castExpression: 'as? String',
43
+ },
44
+ url: {
45
+ type: 'String',
46
+ alwaysOptional: true,
47
+ defaultValue: '""',
48
+ castExpression: 'as? String',
49
+ },
50
+ date: {
51
+ type: 'String',
52
+ alwaysOptional: true,
53
+ defaultValue: '""',
54
+ castExpression: 'as? String',
55
+ },
56
+ image: {
57
+ type: 'ImageValue',
58
+ alwaysOptional: true,
59
+ defaultValue: 'ImageValue(id: "", url: "")',
60
+ castExpression: 'as? [String: Any]',
61
+ needsSharedType: true,
62
+ },
63
+ video: {
64
+ type: 'VideoValue',
65
+ alwaysOptional: true,
66
+ defaultValue: 'VideoValue(id: "", url: "")',
67
+ castExpression: 'as? [String: Any]',
68
+ needsSharedType: true,
69
+ },
70
+ file: {
71
+ type: 'FileValue',
72
+ alwaysOptional: true,
73
+ defaultValue: 'FileValue(id: "", url: "", name: "", size: 0, mimeType: "")',
74
+ castExpression: 'as? [String: Any]',
75
+ needsSharedType: true,
76
+ },
77
+ currency: {
78
+ type: 'CurrencyValue',
79
+ alwaysOptional: true,
80
+ defaultValue: 'CurrencyValue(amount: 0, currency: "")',
81
+ castExpression: 'as? [String: Any]',
82
+ needsSharedType: true,
83
+ },
84
+ select: {
85
+ type: 'String',
86
+ alwaysOptional: true,
87
+ defaultValue: '""',
88
+ castExpression: 'as? String',
89
+ },
90
+ multiselect: {
91
+ type: '[String]',
92
+ alwaysOptional: true,
93
+ defaultValue: '[]',
94
+ castExpression: 'as? [String]',
95
+ },
96
+ json: {
97
+ type: 'Any',
98
+ alwaysOptional: true,
99
+ defaultValue: 'nil',
100
+ castExpression: '',
101
+ },
102
+ list: {
103
+ type: '[Any]',
104
+ alwaysOptional: true,
105
+ defaultValue: '[]',
106
+ castExpression: 'as? [Any]',
107
+ },
108
+ tree: {
109
+ type: '[Any]',
110
+ alwaysOptional: true,
111
+ defaultValue: '[]',
112
+ castExpression: 'as? [Any]',
113
+ },
114
+ flexible: {
115
+ type: '[[String: Any]]',
116
+ alwaysOptional: true,
117
+ defaultValue: '[]',
118
+ castExpression: 'as? [[String: Any]]',
119
+ },
120
+ 'entity-reference': {
121
+ type: 'String',
122
+ alwaysOptional: true,
123
+ defaultValue: '""',
124
+ castExpression: 'as? String',
125
+ },
126
+ reference: {
127
+ type: 'String',
128
+ alwaysOptional: true,
129
+ defaultValue: '""',
130
+ castExpression: 'as? String',
131
+ },
132
+ link: {
133
+ type: 'LinkValue',
134
+ alwaysOptional: true,
135
+ defaultValue: 'LinkValue(type: "")',
136
+ castExpression: 'as? [String: Any]',
137
+ needsSharedType: true,
138
+ },
139
+ };
140
+ /**
141
+ * Get the Swift type for a field.
142
+ * Returns { type, isOptional } so callers can decide on `?` suffix.
143
+ */
144
+ export function getSwiftFieldType(field) {
145
+ const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
146
+ if (!mapping) {
147
+ // Unknown type — treat as optional Any
148
+ return {
149
+ type: 'Any',
150
+ isOptional: true,
151
+ mapping: undefined,
152
+ };
153
+ }
154
+ const isOptional = mapping.alwaysOptional || !field.required;
155
+ return { type: mapping.type, isOptional, mapping };
156
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"write-files.d.ts","sourceRoot":"","sources":["../../src/codegen/write-files.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAc,GAC1B,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,WAAW,GAAE,OAAc,GAC1B,OAAO,CAAC,IAAI,CAAC,CAIf"}
1
+ {"version":3,"file":"write-files.d.ts","sourceRoot":"","sources":["../../src/codegen/write-files.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAc,GAC1B,OAAO,CAAC,IAAI,CAAC,CAsBf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,WAAW,GAAE,OAAc,GAC1B,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -9,7 +9,8 @@ import { dirname } from 'path';
9
9
  export async function writeGeneratedFile(filePath, content, usePrettier = true) {
10
10
  await mkdir(dirname(filePath), { recursive: true });
11
11
  let formattedContent = content;
12
- if (usePrettier) {
12
+ const isSwift = filePath.endsWith('.swift');
13
+ if (usePrettier && !isSwift) {
13
14
  try {
14
15
  const prettier = await import('prettier');
15
16
  const parser = filePath.endsWith('.graphql') ? 'graphql' : 'typescript';
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { GlobalOptions } from '../lib/config.js';
3
+ export declare function registerCustomerProfilesCommands(program: Command, globalOpts: () => GlobalOptions): void;
4
+ //# sourceMappingURL=customer-profiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-profiles.d.ts","sourceRoot":"","sources":["../../src/commands/customer-profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAYtD,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAgIN"}
@@ -0,0 +1,97 @@
1
+ import { withErrorHandler } from '../lib/errors.js';
2
+ import { createClient } from '../lib/client.js';
3
+ import { formatOutput, success } from '../lib/output.js';
4
+ import { parseInputData } from '../lib/input.js';
5
+ import { CUSTOMER_PROFILE_SCHEMA, UPDATE_CUSTOMER_PROFILE_SCHEMA, CUSTOMER_PROFILE, SET_CUSTOMER_PROFILE, } from '../graphql/queries.js';
6
+ export function registerCustomerProfilesCommands(program, globalOpts) {
7
+ const cp = program
8
+ .command('customer-profiles')
9
+ .alias('cp')
10
+ .description('Manage customer profile schema and profiles');
11
+ // --- Schema subcommands ---
12
+ const schema = cp
13
+ .command('schema')
14
+ .description('Manage the customer profile schema');
15
+ // schema get
16
+ schema
17
+ .command('get')
18
+ .description('Get the customer profile schema for the current project')
19
+ .action(withErrorHandler(globalOpts, async () => {
20
+ const opts = globalOpts();
21
+ const client = await createClient(opts);
22
+ const data = await client.request(CUSTOMER_PROFILE_SCHEMA);
23
+ if (!data.customerProfileSchema) {
24
+ if (opts.json || opts.jsonl) {
25
+ formatOutput(null, opts);
26
+ }
27
+ else {
28
+ console.log('No customer profile schema defined. Use "customer-profiles schema update" to create one.');
29
+ }
30
+ return;
31
+ }
32
+ formatOutput(data.customerProfileSchema, opts);
33
+ }));
34
+ // schema update
35
+ schema
36
+ .command('update')
37
+ .description('Update the customer profile schema (fields array)')
38
+ .option('-d, --data <json>', 'Fields array as JSON')
39
+ .option('-f, --file <path>', 'Read fields from file')
40
+ .action(withErrorHandler(globalOpts, async (cmdOpts) => {
41
+ const opts = globalOpts();
42
+ const client = await createClient(opts);
43
+ const inputData = await parseInputData(cmdOpts);
44
+ // Accept either { fields: [...] } or a raw array
45
+ const fields = Array.isArray(inputData)
46
+ ? inputData
47
+ : (inputData.fields ?? inputData);
48
+ if (!Array.isArray(fields)) {
49
+ throw new Error('Input must be a fields array or an object with a "fields" key. ' +
50
+ 'Example: [{"key":"name","label":"Name","type":"text"}]');
51
+ }
52
+ const data = await client.request(UPDATE_CUSTOMER_PROFILE_SCHEMA, { fields });
53
+ formatOutput(data.updateCustomerProfileSchema, opts);
54
+ if (!(opts.json || opts.jsonl || opts.quiet)) {
55
+ const version = data.updateCustomerProfileSchema.version;
56
+ success(`Updated customer profile schema (version ${version})`);
57
+ }
58
+ }));
59
+ // --- Profile subcommands ---
60
+ const profile = cp
61
+ .command('profile')
62
+ .description('Manage individual customer profiles');
63
+ // profile get
64
+ profile
65
+ .command('get <customerId>')
66
+ .description("Get a customer's profile data")
67
+ .action(withErrorHandler(globalOpts, async (customerId) => {
68
+ const opts = globalOpts();
69
+ const client = await createClient(opts);
70
+ const data = await client.request(CUSTOMER_PROFILE, { customerId });
71
+ if (!data.customerProfile) {
72
+ throw new Error(`No profile found for customer "${customerId}".`);
73
+ }
74
+ formatOutput(data.customerProfile, opts);
75
+ }));
76
+ // profile set
77
+ profile
78
+ .command('set <customerId>')
79
+ .description("Set a customer's profile data")
80
+ .option('-d, --data <json>', 'Profile data as JSON')
81
+ .option('-f, --file <path>', 'Read data from file')
82
+ .action(withErrorHandler(globalOpts, async (customerId, cmdOpts) => {
83
+ const opts = globalOpts();
84
+ const client = await createClient(opts);
85
+ const inputData = await parseInputData(cmdOpts);
86
+ // Accept either { data: {...} } or the raw data object
87
+ const profileData = inputData.data ?? inputData;
88
+ const data = await client.request(SET_CUSTOMER_PROFILE, {
89
+ customerId,
90
+ data: profileData,
91
+ });
92
+ formatOutput(data.setCustomerProfile, opts);
93
+ if (!(opts.json || opts.jsonl || opts.quiet)) {
94
+ success(`Set profile for customer ${customerId}`);
95
+ }
96
+ }));
97
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAYtD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CAmHN"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/commands/pull.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAqBtD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CA8KN"}
@@ -3,22 +3,29 @@ import chalk from 'chalk';
3
3
  import { withErrorHandler } from '../lib/errors.js';
4
4
  import { createClient } from '../lib/client.js';
5
5
  import { loadPullConfig } from '../config/pull-config.js';
6
- import { fetchModelsForCodegen, filterModels } from '../codegen/fetch-models.js';
6
+ import { fetchModelsForCodegen, filterModels, } from '../codegen/fetch-models.js';
7
+ import { fetchCustomerProfileSchema } from '../codegen/fetch-customer-profile-schema.js';
8
+ import { toPascalCase } from '../codegen/field-mapping.js';
7
9
  import { generateFieldTypesFile } from '../codegen/generators/field-types.js';
8
10
  import { generateConfigFile } from '../codegen/generators/config.js';
9
11
  import { generateModelTypes } from '../codegen/generators/model-types.js';
10
12
  import { generateModelIndex } from '../codegen/generators/model-index.js';
11
13
  import { generateModelDocuments } from '../codegen/generators/documents.js';
14
+ import { generateSwiftModelFile } from '../codegen/generators/swift-types.js';
15
+ import { generateSwiftFieldTypesFile } from '../codegen/generators/swift-field-types.js';
16
+ import { generateSwiftModelKeys } from '../codegen/generators/swift-model-keys.js';
17
+ import { generateCustomerProfileTypes } from '../codegen/generators/customer-profile-types.js';
12
18
  import { writeFiles } from '../codegen/write-files.js';
13
19
  export function registerPullCommand(program, globalOpts) {
14
20
  program
15
21
  .command('pull')
16
- .description('Generate TypeScript types and GraphQL documents from platform models')
22
+ .description('Generate TypeScript types, GraphQL documents, and Swift types from platform models')
17
23
  .option('--config <path>', 'Path to config file')
18
24
  .option('--only <models>', 'Comma-separated model keys to generate')
19
25
  .option('--no-prettier', 'Skip Prettier formatting')
20
26
  .option('--dry-run', 'Show what would be generated without writing')
21
27
  .option('--out <dir>', 'Override output directory for types')
28
+ .option('--swift <dir>', 'Generate Swift files to directory')
22
29
  .action(withErrorHandler(globalOpts, async (cmdOpts) => {
23
30
  const opts = globalOpts();
24
31
  const flags = {
@@ -27,13 +34,17 @@ export function registerPullCommand(program, globalOpts) {
27
34
  noPrettier: cmdOpts.prettier === false,
28
35
  dryRun: !!cmdOpts.dryRun,
29
36
  out: cmdOpts.out,
37
+ swift: cmdOpts.swift,
30
38
  };
31
39
  const config = await loadPullConfig(flags);
32
- // Fetch models
40
+ // Fetch models + customer profile schema
33
41
  const client = await createClient(opts);
34
42
  console.log(chalk.dim('Fetching models…'));
35
- const allModels = await fetchModelsForCodegen(client);
36
- if (allModels.length === 0) {
43
+ const [allModels, cpSchema] = await Promise.all([
44
+ fetchModelsForCodegen(client),
45
+ fetchCustomerProfileSchema(client),
46
+ ]);
47
+ if (allModels.length === 0 && !cpSchema) {
37
48
  console.log(chalk.yellow('No models found. Nothing to generate.'));
38
49
  return;
39
50
  }
@@ -68,7 +79,14 @@ export function registerPullCommand(program, globalOpts) {
68
79
  path: resolve(typesDir, 'models', 'index.ts'),
69
80
  content: generateModelIndex(models),
70
81
  });
71
- // 4. Per-model GraphQL documents (only for publicApi models with records)
82
+ // 4. Customer profile types (when schema exists)
83
+ if (cpSchema && cpSchema.fields.length > 0) {
84
+ files.push({
85
+ path: resolve(typesDir, 'customer-profile.ts'),
86
+ content: generateCustomerProfileTypes(cpSchema),
87
+ });
88
+ }
89
+ // 5. Per-model GraphQL documents (only for publicApi models with records)
72
90
  const publicModels = models.filter((m) => m.config.publicApi && m.config.records);
73
91
  for (const model of publicModels) {
74
92
  files.push({
@@ -76,11 +94,34 @@ export function registerPullCommand(program, globalOpts) {
76
94
  content: generateModelDocuments(model),
77
95
  });
78
96
  }
79
- // 5. Main index
97
+ // 6. Main index
98
+ const hasCustomerProfile = !!(cpSchema && cpSchema.fields.length > 0);
80
99
  files.push({
81
100
  path: resolve(typesDir, 'index.ts'),
82
- content: generateMainIndex(),
101
+ content: generateMainIndex(hasCustomerProfile),
83
102
  });
103
+ // 7. Swift output (when configured)
104
+ if (config.output.swift) {
105
+ const swiftDir = resolve(cwd, config.output.swift);
106
+ // Shared types
107
+ files.push({
108
+ path: resolve(swiftDir, 'FieldTypes.swift'),
109
+ content: generateSwiftFieldTypesFile(),
110
+ });
111
+ // Model keys
112
+ files.push({
113
+ path: resolve(swiftDir, 'ModelKeys.swift'),
114
+ content: generateSwiftModelKeys(models),
115
+ });
116
+ // Per-model Swift files
117
+ for (const model of models) {
118
+ const swiftTypeName = toPascalCase(model.key);
119
+ files.push({
120
+ path: resolve(swiftDir, `${swiftTypeName}.swift`),
121
+ content: generateSwiftModelFile(model),
122
+ });
123
+ }
124
+ }
84
125
  // Dry run: list files
85
126
  if (config.dryRun) {
86
127
  console.log(chalk.bold('\nDry run — files that would be generated:\n'));
@@ -96,16 +137,21 @@ export function registerPullCommand(program, globalOpts) {
96
137
  // Summary
97
138
  const modelCount = models.length;
98
139
  const docCount = publicModels.length;
140
+ const swiftCount = config.output.swift ? models.length + 2 : 0; // +2 for FieldTypes + ModelKeys
141
+ const cpSuffix = hasCustomerProfile ? ', customer profile' : '';
99
142
  console.log(chalk.green(`\nGenerated ${files.length} file(s)`) +
100
- chalk.dim(` (${modelCount} model type(s), ${docCount} document(s))`));
143
+ chalk.dim(` (${modelCount} model type(s), ${docCount} document(s)${cpSuffix}${swiftCount > 0 ? `, ${swiftCount} Swift file(s)` : ''})`));
101
144
  console.log(chalk.dim(` Types: ${typesDir}`));
102
145
  if (docCount > 0) {
103
146
  console.log(chalk.dim(` Documents: ${docsDir}`));
104
147
  }
148
+ if (config.output.swift) {
149
+ console.log(chalk.dim(` Swift: ${resolve(cwd, config.output.swift)}`));
150
+ }
105
151
  }));
106
152
  }
107
- function generateMainIndex() {
108
- return `/**
153
+ function generateMainIndex(includeCustomerProfile) {
154
+ let code = `/**
109
155
  * Generated types and configs
110
156
  *
111
157
  * @generated by foir — DO NOT EDIT MANUALLY
@@ -115,4 +161,8 @@ export * from './field-types.js';
115
161
  export * from './config.js';
116
162
  export * from './models/index.js';
117
163
  `;
164
+ if (includeCustomerProfile) {
165
+ code += `export * from './customer-profile.js';\n`;
166
+ }
167
+ return code;
118
168
  }
@@ -7,11 +7,13 @@ export interface PullCliFlags {
7
7
  noPrettier?: boolean;
8
8
  dryRun?: boolean;
9
9
  out?: string;
10
+ swift?: string;
10
11
  }
11
12
  export interface ResolvedPullConfig {
12
13
  output: {
13
14
  types: string;
14
15
  documents: string;
16
+ swift?: string;
15
17
  };
16
18
  only: string[];
17
19
  includeInline: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"pull-config.d.ts","sourceRoot":"","sources":["../../src/config/pull-config.ts"],"names":[],"mappings":"AAAA;;GAEG;AA+CH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,kBAAkB,CAAC,CAuB7B"}
1
+ {"version":3,"file":"pull-config.d.ts","sourceRoot":"","sources":["../../src/config/pull-config.ts"],"names":[],"mappings":"AAAA;;GAEG;AA+CH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,kBAAkB,CAAC,CA4B7B"}