diff --git a/.github/workflows/sync-server-side-sdks-schema.yml b/.github/workflows/sync-server-side-sdks-schema.yml index fbfb7c20..a892c509 100644 --- a/.github/workflows/sync-server-side-sdks-schema.yml +++ b/.github/workflows/sync-server-side-sdks-schema.yml @@ -110,6 +110,7 @@ jobs: language: python language-version: '3.12' generate-command: 'bash ./generate.sh' + schema-source: fingerprint-server-api-v4-flat.yaml schema-path: res/fingerprint-server-api.yaml examples-path: test/mocks app-id: ${{ vars.RUNNER_APP_ID }} @@ -152,6 +153,7 @@ jobs: language: php language-version: '8.3' generate-command: 'bash ./scripts/generate.sh' + schema-source: fingerprint-server-api-v4-flat.yaml schema-path: res/fingerprint-server-api.yaml examples-path: test/mocks app-id: ${{ vars.RUNNER_APP_ID }} diff --git a/utils/mocks/publishedEvent.yaml b/utils/mocks/publishedEvent.yaml new file mode 100644 index 00000000..65b2872f --- /dev/null +++ b/utils/mocks/publishedEvent.yaml @@ -0,0 +1,351 @@ +type: object +description: Contains results from Fingerprint Identification and all active Smart Signals. Some Smart Signals are only supported for certain device types, these fields will be omitted for events not generated from the supported devices. Consult the [Smart Signals reference](https://docs.fingerprint.com/docs/smart-signals-reference) for more details. +additionalProperties: false +required: + - event_id + - timestamp +properties: + event_id: + $ref: platform/EventId.yaml + x-platforms: + - android + - ios + - browser + timestamp: + $ref: platform/Timestamp.yaml + x-platforms: + - android + - ios + - browser + source: + $ref: platform/EventSource.yaml + x-platforms: + - android + - ios + - browser + incremental_identification_status: + $ref: platform/IncrementalIdentificationStatus.yaml + x-platforms: + - browser + linked_id: + $ref: platform/LinkedId.yaml + x-platforms: + - android + - ios + - browser + environment_id: + $ref: platform/EnvironmentId.yaml + x-platforms: + - android + - ios + - browser + suspect: + $ref: platform/Suspect.yaml + x-platforms: + - android + - ios + - browser + sdk: + $ref: platform/SDK.yaml + x-platforms: + - android + - ios + - browser + replayed: + $ref: platform/Replayed.yaml + x-platforms: + - android + - ios + - browser + triggered_by: + $ref: platform/webhook/TriggeredBy.yaml + identification: + $ref: identification/Identification.yaml + x-platforms: + - android + - ios + - browser + supplementary_id_high_recall: + $ref: identification/SupplementaryIDHighRecall.yaml + x-platforms: + - android + - ios + - browser + tags: + $ref: identification/Tags.yaml + x-platforms: + - android + - ios + - browser + url: + $ref: identification/Url.yaml + x-platforms: + - browser + bundle_id: + $ref: identification/BundleId.yaml + x-platforms: + - ios + package_name: + $ref: identification/PackageName.yaml + x-platforms: + - android + ip_address: + $ref: identification/IpAddress.yaml + x-platforms: + - android + - ios + - browser + user_agent: + $ref: identification/UserAgent.yaml + x-platforms: + - android + - ios + - browser + device: + $ref: identification/Device.yaml + x-platforms: + - android + - ios + - browser + os: + $ref: identification/Os.yaml + x-platforms: + - android + - ios + - browser + os_version: + $ref: identification/OsVersion.yaml + x-platforms: + - android + - ios + - browser + client_referrer: + $ref: identification/ClientReferrer.yaml + x-platforms: + - browser + browser_details: + $ref: identification/BrowserDetails.yaml + x-platforms: + - browser + proximity: + $ref: identification/Proximity.yaml + x-platforms: + - android + - ios + - browser + active_call: + $ref: smartsignals/ActiveCall.yaml + x-platforms: + - android + - ios + bot: + $ref: smartsignals/BotResult.yaml + x-platforms: + - browser + bot_type: + $ref: smartsignals/BotType.yaml + x-platforms: + - browser + bot_info: + $ref: smartsignals/BotInfo.yaml + x-platforms: + - browser + cloned_app: + $ref: smartsignals/ClonedApp.yaml + x-platforms: + - android + developer_tools: + $ref: smartsignals/DeveloperTools.yaml + x-platforms: + - browser + - android + - ios + device_details: + $ref: smartsignals/DeviceDetails.yaml + x-platforms: + - android + - ios + emulator: + $ref: smartsignals/Emulator.yaml + x-platforms: + - android + factory_reset_timestamp: + $ref: smartsignals/FactoryReset.yaml + x-platforms: + - android + - ios + frida: + $ref: smartsignals/Frida.yaml + x-platforms: + - android + - ios + ip_blocklist: + $ref: smartsignals/IPBlockList.yaml + x-platforms: + - android + - ios + - browser + ip_info: + $ref: smartsignals/IPInfo.yaml + x-platforms: + - android + - ios + - browser + proxy: + $ref: smartsignals/Proxy.yaml + x-platforms: + - android + - ios + - browser + proxy_confidence: + $ref: smartsignals/ProxyConfidence.yaml + x-platforms: + - android + - ios + - browser + proxy_details: + $ref: smartsignals/ProxyDetails.yaml + x-platforms: + - android + - ios + - browser + proxy_ml_score: + $ref: smartsignals/ProxyMLScore.yaml + x-platforms: + - browser + incognito: + $ref: smartsignals/Incognito.yaml + x-platforms: + - browser + jailbroken: + $ref: smartsignals/Jailbroken.yaml + x-platforms: + - ios + location_spoofing: + $ref: smartsignals/LocationSpoofing.yaml + x-platforms: + - android + - ios + mitm_attack: + $ref: smartsignals/MitMAttack.yaml + x-platforms: + - android + - ios + privacy_settings: + $ref: smartsignals/PrivacySettings.yaml + x-platforms: + - browser + root_apps: + $ref: smartsignals/RootApps.yaml + x-platforms: + - android + rule_action: + $ref: platform/EventRuleAction.yaml + simulator: + $ref: smartsignals/Simulator.yaml + x-platforms: + - ios + suspect_score: + $ref: smartsignals/SuspectScore.yaml + x-platforms: + - android + - ios + - browser + tampering: + $ref: smartsignals/Tampering.yaml + x-platforms: + - android + - ios + - browser + tampering_confidence: + $ref: smartsignals/TamperingConfidence.yaml + x-platforms: + - android + - ios + - browser + tampering_ml_score: + $ref: smartsignals/TamperingMlScore.yaml + x-platforms: + - android + - ios + - browser + tampering_details: + $ref: smartsignals/TamperingDetails.yaml + x-platforms: + - android + - ios + - browser + velocity: + $ref: smartsignals/Velocity.yaml + x-platforms: + - android + - ios + - browser + virtual_machine: + $ref: smartsignals/VirtualMachine.yaml + x-platforms: + - browser + virtual_machine_ml_score: + $ref: smartsignals/VirtualMachineMLScore.yaml + x-platforms: + - browser + vpn: + $ref: smartsignals/Vpn.yaml + x-platforms: + - android + - ios + - browser + vpn_confidence: + $ref: smartsignals/VpnConfidence.yaml + x-platforms: + - android + - ios + - browser + vpn_ml_score: + $ref: smartsignals/VpnMLScore.yaml + x-platforms: + - browser + vpn_origin_timezone: + $ref: smartsignals/VpnOriginTimezone.yaml + x-platforms: + - android + - ios + - browser + vpn_origin_country: + $ref: smartsignals/VpnOriginCountry.yaml + x-platforms: + - android + - ios + - browser + vpn_methods: + $ref: smartsignals/VpnMethods.yaml + x-platforms: + - android + - ios + - browser + high_activity_device: + $ref: smartsignals/HighActivity.yaml + x-platforms: + - android + - ios + - browser + rare_device: + $ref: smartsignals/RareDevice.yaml + x-platforms: + - browser + rare_device_percentile_bucket: + $ref: smartsignals/RareDevicePercentileBucket.yaml + x-platforms: + - browser + raw_device_attributes: + $ref: smartsignals/RawDeviceAttributes.yaml + x-platforms: + - browser + - ios + - android + labels: + $ref: smartsignals/Labels.yaml + x-platforms: + - browser + - ios + - android diff --git a/utils/replaceOneOf.spec.ts b/utils/replaceOneOf.spec.ts index b419ef03..fedb4dc9 100644 --- a/utils/replaceOneOf.spec.ts +++ b/utils/replaceOneOf.spec.ts @@ -455,4 +455,30 @@ describe('Test replaceOneOf', () => { expect(schema.additionalProperties).toBe(true); }); + + it('keeps x-platforms from an earlier variant when a later variant omits them', () => { + const schema: OpenApiDocument = { + oneOf: [ + { + type: 'object', + properties: { + url: { type: 'string', 'x-platforms': ['browser'] }, + tags: { type: 'object', 'x-platforms': ['browser'] }, + }, + }, + { + type: 'object', + properties: { + url: { type: 'string' }, + tags: { type: 'object', 'x-platforms': ['ios'] }, + }, + }, + ], + }; + + replaceOneOf(schema, {}, 'oneOf'); + + expect(schema.properties.url['x-platforms']).toEqual(['browser']); + expect(schema.properties.tags['x-platforms']).toEqual(['ios']); + }); }); diff --git a/utils/replaceOneOf.ts b/utils/replaceOneOf.ts index cf83c9de..1a6121d1 100644 --- a/utils/replaceOneOf.ts +++ b/utils/replaceOneOf.ts @@ -42,8 +42,14 @@ export function replaceOneOf( requiredCounts[propName] = (requiredCounts[propName] || 0) + 1; } - // Clone the property to avoid mutating the original schema - properties[propName] = structuredClone(currentItem.properties[propName]); + // Clone the property to avoid mutating the original schema. + // Keep x-platforms from an earlier variant when a later one omits them (EventEdge has none). + const property = structuredClone(currentItem.properties[propName]); + const previousPlatforms = properties[propName]?.['x-platforms']; + if (previousPlatforms !== undefined && property['x-platforms'] === undefined) { + property['x-platforms'] = previousPlatforms; + } + properties[propName] = property; if (!literalValues[propName]) { literalValues[propName] = []; diff --git a/utils/transformers/flattenNamedSchemaOneOfTransformer.spec.ts b/utils/transformers/flattenNamedSchemaOneOfTransformer.spec.ts new file mode 100644 index 00000000..4ace55d2 --- /dev/null +++ b/utils/transformers/flattenNamedSchemaOneOfTransformer.spec.ts @@ -0,0 +1,110 @@ +import { flattenNamedSchemaOneOfTransformer } from './flattenNamedSchemaOneOfTransformer.ts'; +import type { OpenApiDocument } from '../openapi.ts'; + +function eventUnionSchema(): OpenApiDocument { + return { + components: { + schemas: { + EventSource: { type: 'string', enum: ['device', 'edge'] }, + EventDevice: { + type: 'object', + required: ['event_id', 'source'], + properties: { + event_id: { type: 'string' }, + source: { + $ref: '#/components/schemas/EventSource', + const: 'device', + 'x-platforms': ['android', 'ios', 'browser'], + }, + identification: { type: 'object' }, + }, + }, + EventEdge: { + type: 'object', + required: ['event_id', 'source', 'ip_info'], + properties: { + event_id: { type: 'string' }, + source: { $ref: '#/components/schemas/EventSource', const: 'edge' }, + ip_info: { type: 'object' }, + }, + }, + Event: { + oneOf: [{ $ref: '#/components/schemas/EventDevice' }, { $ref: '#/components/schemas/EventEdge' }], + discriminator: { + propertyName: 'source', + mapping: { + device: '#/components/schemas/EventDevice', + edge: '#/components/schemas/EventEdge', + }, + }, + }, + EventRuleAction: { + oneOf: [{ $ref: '#/components/schemas/Allow' }, { $ref: '#/components/schemas/Block' }], + }, + Allow: { type: 'object', properties: { type: { const: 'allow' } } }, + Block: { type: 'object', properties: { type: { const: 'block' } } }, + }, + }, + }; +} + +describe('flattenNamedSchemaOneOfTransformer', () => { + it('merges the named oneOf into one object', () => { + const schema = eventUnionSchema(); + + flattenNamedSchemaOneOfTransformer('Event')(schema); + + const event = schema.components.schemas.Event; + expect(event.oneOf).toBeUndefined(); + expect(event.discriminator).toBeUndefined(); + expect(Object.keys(event.properties)).toEqual(['event_id', 'source', 'identification', 'ip_info']); + expect(event.required).toEqual(['event_id', 'source']); + }); + + it('keeps the discriminator as a plain $ref with the earlier variant x-platforms', () => { + const schema = eventUnionSchema(); + + flattenNamedSchemaOneOfTransformer('Event')(schema); + + expect(schema.components.schemas.Event.properties.source).toEqual({ + $ref: '#/components/schemas/EventSource', + 'x-platforms': ['android', 'ios', 'browser'], + }); + }); + + it('drops optionalProperties from required', () => { + const schema = eventUnionSchema(); + + flattenNamedSchemaOneOfTransformer('Event', { optionalProperties: ['source'] })(schema); + + expect(schema.components.schemas.Event.required).toEqual(['event_id']); + }); + + it('replaces the description when one is given', () => { + const schema = eventUnionSchema(); + + flattenNamedSchemaOneOfTransformer('Event', { description: 'A flat event.' })(schema); + + expect(schema.components.schemas.Event.description).toBe('A flat event.'); + }); + + it('does not touch other oneOf schemas', () => { + const schema = eventUnionSchema(); + const original = structuredClone(schema.components.schemas.EventRuleAction); + + flattenNamedSchemaOneOfTransformer('Event')(schema); + + expect(schema.components.schemas.EventRuleAction).toEqual(original); + }); + + it('does nothing when the named schema has no oneOf', () => { + const schema: OpenApiDocument = { + components: { schemas: { Event: { type: 'object', properties: { event_id: { type: 'string' } } } } }, + }; + const original = structuredClone(schema); + + flattenNamedSchemaOneOfTransformer('Event')(schema); + + expect(schema).toEqual(original); + }); +}); diff --git a/utils/transformers/flattenNamedSchemaOneOfTransformer.ts b/utils/transformers/flattenNamedSchemaOneOfTransformer.ts new file mode 100644 index 00000000..cca94867 --- /dev/null +++ b/utils/transformers/flattenNamedSchemaOneOfTransformer.ts @@ -0,0 +1,55 @@ +import { replaceOneOf } from '../replaceOneOf.ts'; +import type { Transformer } from '../openapi.ts'; + +export type FlattenNamedSchemaOneOfOptions = { + /** + * Leave these properties optional on the flattened object even if every + * variant requires them. Keeps `Event.source` optional in SDK schemas. + */ + optionalProperties?: string[]; + /** + * Replaces the union's description, which usually describes the variants and doesn't fit a flat object. + */ + description?: string; +}; + +/** + * Flatten `oneOf` on one named schema, leaving other unions (e.g. `EventRuleAction`) intact. + * Turns `Event` (`EventDevice | EventEdge`) back into a single object for SDKs that can't model the union. + * + * Run it before `liftOneOfSharedPropertiesTransformer` and `resolveAllOfTransformer`. Variants define the + * discriminator as `$ref` + sibling `const`; those transformers would inline it into `type: string, enum`. + */ +export function flattenNamedSchemaOneOfTransformer( + schemaName: string, + { optionalProperties = [], description }: FlattenNamedSchemaOneOfOptions = {} +): Transformer { + return (apiDefinition) => { + const schemas = apiDefinition.components?.schemas; + const schema = schemas?.[schemaName]; + if (!schema?.oneOf) { + return; + } + + replaceOneOf(schema, schemas, 'oneOf'); + + if (description !== undefined) { + schema.description = description; + } + + // replaceOneOf merges the variants' `const`s into an `enum` next to the `$ref`. Keep the plain `$ref`. + for (const property of Object.values>(schema.properties ?? {})) { + if (typeof property.$ref === 'string') { + delete property.enum; + delete property.const; + } + } + + if (Array.isArray(schema.required)) { + schema.required = schema.required.filter((name: string) => !optionalProperties.includes(name)); + if (schema.required.length === 0) { + delete schema.required; + } + } + }; +} diff --git a/utils/transformers/transformSchema.spec.ts b/utils/transformers/transformSchema.spec.ts index 7dcde8d2..31f180da 100644 --- a/utils/transformers/transformSchema.spec.ts +++ b/utils/transformers/transformSchema.spec.ts @@ -4,6 +4,7 @@ import { transformSchema, v4Transformers, v4SchemaForSdksTransformers, + v4SchemaForSdksFlatTransformers, v4SchemaForSdksNormalizedTransformers, } from './transformSchema.ts'; import type { OpenApiDocument } from '../openapi.ts'; @@ -116,7 +117,11 @@ describe('Test transformSchema pipelines for v4', () => { components: { schemas: {} }, }); - for (const transformers of [v4SchemaForSdksTransformers, v4SchemaForSdksNormalizedTransformers]) { + for (const transformers of [ + v4SchemaForSdksTransformers, + v4SchemaForSdksFlatTransformers, + v4SchemaForSdksNormalizedTransformers, + ]) { const result = transformSchema(yamlWithEdge, transformers); const parsed = parseYaml(result); expect(parsed.paths['/edge']).toBeUndefined(); @@ -163,4 +168,40 @@ describe('Test transformSchema pipelines for v4', () => { expect(hasYamlKey(pathsYaml, 'oneOf')).toBe(false); }); + + // `Event` as published before it became `EventDevice | EventEdge` (main:schemas/components/schemas/Event.yaml). + // SDKs on the flat and normalized schemas must keep the same properties, order and refs. + describe('flat and normalized SDK schemas keep the published Event shape', () => { + const publishedEvent = parseYaml(fs.readFileSync('./utils/mocks/publishedEvent.yaml')); + const toComponentRef = (fileRef: string) => fileRef.replace(/^(.*\/)?(\w+)\.yaml$/, '#/components/schemas/$2'); + const publishedProperties = Object.entries(publishedEvent.properties) + .filter(([name]) => name !== 'triggered_by') + .map(([name, property]) => ({ + name, + $ref: toComponentRef(property.$ref), + 'x-platforms': property['x-platforms'], + })); + + it.each([ + ['v4-flat', v4SchemaForSdksFlatTransformers], + ['v4-normalized', v4SchemaForSdksNormalizedTransformers], + ])('%s', (_, transformers) => { + const schemas = parseYaml(transformSchema(v4Schema, transformers)).components.schemas; + const event = schemas.Event; + + expect(event.oneOf).toBeUndefined(); + expect( + Object.entries(event.properties).map(([name, property]) => ({ + name, + $ref: property.$ref, + 'x-platforms': property['x-platforms'], + })) + ).toEqual(publishedProperties); + expect(event.required).toEqual(publishedEvent.required); + // The union's description ("discriminated by its `source` property") doesn't fit a flat object. + expect(event.description).toMatch(/^Contains results from Fingerprint Identification/); + expect(event.description).not.toMatch(/discriminated/); + expect(schemas.EventRuleAction.oneOf).toBeDefined(); + }); + }); }); diff --git a/utils/transformers/transformSchema.ts b/utils/transformers/transformSchema.ts index 0d2ddad4..1b4846c8 100644 --- a/utils/transformers/transformSchema.ts +++ b/utils/transformers/transformSchema.ts @@ -4,6 +4,7 @@ import { addXReadmeTransformer } from './addXReadmeTransformer.ts'; import { appendExternalSchemaRefTransformer } from './appendExternalSchemaRefTransformer.ts'; import { extractFirstParameterExampleTransformer } from './extractFirstParameterExampleTransformer.ts'; import { extractPathOperationInlineEnumsTransformer } from './extractPathOperationInlineEnumsTransformer.ts'; +import { flattenNamedSchemaOneOfTransformer } from './flattenNamedSchemaOneOfTransformer.ts'; import { parseYaml } from './parseYaml.ts'; import { removeBigExamplesTransformer } from './removeBigExamplesTransformer.ts'; import { removeDeprecatedResponsesTransformer } from './removeDeprecatedResponsesTransformer.ts'; @@ -23,32 +24,30 @@ import { inlineReferencedPropertiesTransformer } from './inlineReferencedPropert import { replaceStartEndQueryParameters } from './replaceStartEndQueryParameters.ts'; import type { OpenApiDocument, Transformer } from '../openapi.ts'; -export const commonTransformers: Transformer[] = [ +const loadTransformers: Transformer[] = [ resolveRefTransformer({ schemaPath: './schemas' }), resolveExternalValueTransformer({ examplesPath: './schemas/paths/' }), removeFieldTransformer('triggered_by'), - liftOneOfSharedPropertiesTransformer, - resolveAllOfTransformer, ]; -const defaultTransformers: Transformer[] = [...commonTransformers]; +const resolveCompositionTransformers: Transformer[] = [liftOneOfSharedPropertiesTransformer, resolveAllOfTransformer]; -export const v4CommonTransformers: Transformer[] = [ - ...commonTransformers, - removeFieldsByPrefixTransformer('x-ruleset-'), -]; +export const commonTransformers: Transformer[] = [...loadTransformers, ...resolveCompositionTransformers]; -export const v4Transformers: Transformer[] = [ - ...v4CommonTransformers, - // Hide deprecated responses from the docs. SDK schemas keep them to avoid breaking changes. - removeDeprecatedResponsesTransformer, - // This transformer should run last to ensure all unused schemas are found - removeUnusedSchemasTransformer, -]; +// Flatten `Event` (`EventDevice | EventEdge`) into one object with optional `source`. +// Must run before `oneOf`/`allOf` are rewritten, or `source` is inlined as an enum instead of `$ref: EventSource`. +const flattenEventTransformer = flattenNamedSchemaOneOfTransformer('Event', { + optionalProperties: ['source'], + description: `Contains results from Fingerprint Identification and all active Smart Signals, or from the Automation Intelligence API. Check \`source\` to see which properties can be present: +- \`device\` - generated by the JS agent or a mobile SDK running on an end-user device. Contains Identification (\`visitor_id\`) and Smart Signals. Some Smart Signals are only supported for certain device types, these fields will be omitted for events not generated from the supported devices. +- \`edge\` - generated by the Automation Intelligence API (\`/edge\` endpoint). Contains only IP and bot analysis, never Identification or Smart Signals derived from device telemetry. -export const v4SchemaForSdksCommonTransformers: Transformer[] = [ - ...v4CommonTransformers, - removeUnusedSchemasTransformer, +Events stored before \`source\` was added don't have it; treat them as \`device\`. Consult the [Smart Signals reference](https://docs.fingerprint.com/docs/smart-signals-reference) for more details. +`, +}); + +const v4SdkTransformers: Transformer[] = [ + removeFieldsByPrefixTransformer('x-ruleset-'), extractFirstParameterExampleTransformer, removeEdgeTransformer, removeFeedbackTransformer, @@ -58,10 +57,6 @@ export const v4SchemaForSdksCommonTransformers: Transformer[] = [ removeFieldTransformer('x-readme'), removeFieldTransformer('additionalProperties', false), removeBigExamplesTransformer, -]; - -export const v4SchemaForSdksTransformers: Transformer[] = [ - ...v4SchemaForSdksCommonTransformers, // Inline enums previously extracted from BotInfo to avoid breaking changes in the SDKs using this schema inlineReferencedPropertiesTransformer('BotInfo'), // Remove the added enum attribute for BotInfo.category. This must follow the inline transformer on the previous line. @@ -70,18 +65,37 @@ export const v4SchemaForSdksTransformers: Transformer[] = [ removeUnusedSchemasTransformer, ]; -export const v4SchemaForSdksNormalizedTransformers: Transformer[] = [ - ...v4SchemaForSdksCommonTransformers, - // Expand oneOf query parameters, start and end, to avoid breaking changes in the SDKs using this schema - replaceStartEndQueryParameters(), - // Inline enums previously extracted from BotInfo to avoid breaking changes in the SDKs using this schema - inlineReferencedPropertiesTransformer('BotInfo'), - // Remove the added enum attribute for BotInfo.category. This must follow the inline transformer on the previous line. - removeFieldByPathTransformer(['components', 'schemas', 'BotInfo', 'properties', 'category', 'enum']), +export const v4Transformers: Transformer[] = [ + ...loadTransformers, + ...resolveCompositionTransformers, + removeFieldsByPrefixTransformer('x-ruleset-'), + // Hide deprecated responses from the docs. SDK schemas keep them to avoid breaking changes. + removeDeprecatedResponsesTransformer, // This transformer should run last to ensure all unused schemas are found removeUnusedSchemasTransformer, ]; +// Node SDK: `Event` stays `EventDevice | EventEdge` +export const v4SchemaForSdksTransformers: Transformer[] = [ + ...loadTransformers, + ...resolveCompositionTransformers, + ...v4SdkTransformers, +]; + +// Python, PHP: flat `Event` +export const v4SchemaForSdksFlatTransformers: Transformer[] = [ + ...loadTransformers, + flattenEventTransformer, + ...resolveCompositionTransformers, + ...v4SdkTransformers, +]; + +// Go, Java, .NET: flat `Event`, then split start/end query parameters +export const v4SchemaForSdksNormalizedTransformers: Transformer[] = [ + ...v4SchemaForSdksFlatTransformers, + replaceStartEndQueryParameters(), +]; + export const readmeApiExplorerTransformers: Transformer[] = [ ...commonTransformers, removeWebhookTransformer, @@ -94,13 +108,13 @@ export const relatedVisitorsApiTransformers: Transformer[] = [ ]; export const removeExtraDocumentationTransformers: Transformer[] = [ - ...defaultTransformers, + ...commonTransformers, removeBigExamplesTransformer, removeFieldTransformer('x-readme'), ]; export const schemaForSdksTransformers: Transformer[] = [ - ...defaultTransformers, + ...commonTransformers, replaceTagsTransformer, removeFieldTransformer('webhooks'), removeFieldTransformer('x-readme'), @@ -108,7 +122,7 @@ export const schemaForSdksTransformers: Transformer[] = [ removeBigExamplesTransformer, ]; -export function transformSchema(content: string | Buffer, transformers: Transformer[] = defaultTransformers): string { +export function transformSchema(content: string | Buffer, transformers: Transformer[]): string { try { const apiDefinition: OpenApiDocument = parseYaml(content); diff --git a/vite.config.ts b/vite.config.ts index 0d71d8fe..26c71d63 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,11 +2,13 @@ import 'dotenv/config'; import { defineConfig, type Plugin } from 'vite'; import { viteStaticCopy, type Target } from 'vite-plugin-static-copy'; import { + commonTransformers, readmeApiExplorerTransformers, relatedVisitorsApiTransformers, removeExtraDocumentationTransformers, schemaForSdksTransformers, transformSchema, + v4SchemaForSdksFlatTransformers, v4SchemaForSdksNormalizedTransformers, v4SchemaForSdksTransformers, v4Transformers, @@ -26,18 +28,26 @@ const schemaOutputs = [ }, { from: 'schemas/fingerprint-server-api-v4.yaml', - // just schema used by most SDKs + // schema used by the Node SDK // examples are removed - // includes `oneOf` operators same as the source schema + // includes `oneOf` operators same as the source schema, including `Event` (`EventDevice | EventEdge`) // additionalProperties: false are removed for backward compatibility to: 'fingerprint-server-api-v4.yaml', transformers: v4SchemaForSdksTransformers, }, + { + from: 'schemas/fingerprint-server-api-v4.yaml', + // schema used by the Python and PHP SDKs + // same as fingerprint-server-api-v4.yaml, but `Event` is flattened into one object with optional `source` + to: 'fingerprint-server-api-v4-flat.yaml', + transformers: v4SchemaForSdksFlatTransformers, + }, { from: 'schemas/fingerprint-server-api-v4.yaml', // normalized schema used by SDKs in weakly typed languages // examples are removed // `oneOf` query parameters are split into two or more parameters + // `Event` is flattened into one object with optional `source` // additionalProperties: false are removed for backward compatibility to: 'fingerprint-server-api-v4-normalized.yaml', transformers: v4SchemaForSdksNormalizedTransformers, @@ -45,7 +55,7 @@ const schemaOutputs = [ { from: 'schemas/fingerprint-server-api-for-sdks.yaml', to: 'fingerprint-server-api.yaml', - transformers: undefined, + transformers: commonTransformers, }, { from: 'schemas/fingerprint-related-visitors-api-readme-explorer.yaml', @@ -98,8 +108,7 @@ export default defineConfig({ src: from, dest: 'schemas', rename: { stripBase: true, name: to }, - transform: (content: string) => - transformers ? transformSchema(content, transformers) : transformSchema(content), + transform: (content: string) => transformSchema(content, transformers), })), { src: ['schemas/paths/examples/**/*', '!schemas/paths/examples/edge/**'],