From e4d7ef5cd2999ad14fae79971dda31d4d9ba2bb8 Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Mon, 11 Sep 2023 21:41:24 -0700 Subject: [PATCH 01/11] minimal working version with CLI flag and unit tests --- packages/cli/src/commands/mock.ts | 9 +++- packages/cli/src/commands/proxy.ts | 1 + packages/cli/src/util/createServer.ts | 8 +-- .../fixtures/invalid-examples.oas3.yaml | 48 +++++++++++++++++ .../src/__tests__/server.oas.spec.ts | 52 ++++++++++++++++++- packages/http-server/src/server.ts | 30 ++++++++--- packages/http/src/mocker/index.ts | 28 ++++++---- .../mocker/negotiator/NegotiatorHelpers.ts | 17 +++--- packages/http/src/types.ts | 2 + 9 files changed, 163 insertions(+), 32 deletions(-) create mode 100644 packages/http-server/src/__tests__/fixtures/invalid-examples.oas3.yaml diff --git a/packages/cli/src/commands/mock.ts b/packages/cli/src/commands/mock.ts index d6fc6ac44..f9a50358a 100644 --- a/packages/cli/src/commands/mock.ts +++ b/packages/cli/src/commands/mock.ts @@ -25,10 +25,16 @@ const mockCommand: CommandModule = { default: undefined, boolean: true, }, + defaultExamples: { + alias: 'de', + description: 'Generates dynamic default response examples in place of schema examples that are invalid.', + boolean: true, + default: false, + }, }), handler: parsedArgs => { parsedArgs.jsonSchemaFakerFillProperties = parsedArgs['json-schema-faker-fillProperties']; - const { multiprocess, dynamic, port, host, cors, document, errors, verboseLevel, jsonSchemaFakerFillProperties } = + const { multiprocess, dynamic, port, host, cors, document, errors, verboseLevel, defaultExamples, jsonSchemaFakerFillProperties } = parsedArgs as unknown as CreateMockServerOptions; const createPrism = multiprocess ? createMultiProcessPrism : createSingleProcessPrism; @@ -41,6 +47,7 @@ const mockCommand: CommandModule = { multiprocess, errors, verboseLevel, + defaultExamples, jsonSchemaFakerFillProperties, }; diff --git a/packages/cli/src/commands/proxy.ts b/packages/cli/src/commands/proxy.ts index a806d3db8..da5ddf0ae 100644 --- a/packages/cli/src/commands/proxy.ts +++ b/packages/cli/src/commands/proxy.ts @@ -51,6 +51,7 @@ const proxyCommand: CommandModule = { 'errors', 'validateRequest', 'verboseLevel', + 'defaultExamples', 'upstreamProxy', 'jsonSchemaFakerFillProperties' ); diff --git a/packages/cli/src/util/createServer.ts b/packages/cli/src/util/createServer.ts index 11f1231c2..e9feb9db4 100644 --- a/packages/cli/src/util/createServer.ts +++ b/packages/cli/src/util/createServer.ts @@ -85,9 +85,9 @@ async function createPrismServerWithLogger(options: CreateBaseServerOptions, log validateRequest, validateResponse: true, checkSecurity: true, - errors: false, + errors: options.errors, upstreamProxy: undefined, - mock: { dynamic: options.dynamic }, + mock: { dynamic: options.dynamic, defaultExamples: options.defaultExamples }, }; const config: IHttpConfig = isProxyServerOptions(options) @@ -95,10 +95,9 @@ async function createPrismServerWithLogger(options: CreateBaseServerOptions, log ...shared, isProxy: true, upstream: options.upstream, - errors: options.errors, upstreamProxy: options.upstreamProxy, } - : { ...shared, isProxy: false, errors: options.errors }; + : { ...shared, isProxy: false }; const server = createHttpServer(operations, { cors: options.cors, @@ -155,6 +154,7 @@ type CreateBaseServerOptions = { multiprocess: boolean; errors: boolean; verboseLevel: string; + defaultExamples: boolean; jsonSchemaFakerFillProperties: boolean; }; diff --git a/packages/http-server/src/__tests__/fixtures/invalid-examples.oas3.yaml b/packages/http-server/src/__tests__/fixtures/invalid-examples.oas3.yaml new file mode 100644 index 000000000..eaf89a8ee --- /dev/null +++ b/packages/http-server/src/__tests__/fixtures/invalid-examples.oas3.yaml @@ -0,0 +1,48 @@ +openapi: 3.0.2 +paths: + /pets: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + '$ref': '#/schemas/Pet' + examples: + invalid_cat: + summary: An invalid example of a cat + value: + id: 135 + status: "new" + invalid_dog: + summary: An invalid example of a dog + value: + id: 135 + name: 123 + valid_dog: + summary: A valid example of a dog + value: + id: 135 + name: "dog" + name: "available" + +schemas: + Pet: + type: object + required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + example: doggie + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold \ No newline at end of file diff --git a/packages/http-server/src/__tests__/server.oas.spec.ts b/packages/http-server/src/__tests__/server.oas.spec.ts index 95a4e7a7b..c70be7a8e 100644 --- a/packages/http-server/src/__tests__/server.oas.spec.ts +++ b/packages/http-server/src/__tests__/server.oas.spec.ts @@ -6,6 +6,7 @@ import { merge } from 'lodash'; import fetch, { RequestInit } from 'node-fetch'; import { createServer } from '../'; import { ThenArg } from '../types'; +import { WithUnknownContainers } from 'io-ts/lib/Schemable'; const logger = createLogger('TEST', { enabled: false }); @@ -136,11 +137,60 @@ describe('Prefer header overrides', () => { }); }); +describe('Invalid examples', () => { + let server: ThenArg>; + const schema = { id: 'number', name: 'string', status: 'string' }; + + interface PayloadSchema { + [key: string]: string; + } + + function hasPropertiesOfType(obj: any, schema: PayloadSchema): boolean { + return Object.keys(schema).every(key => typeof obj[key] === schema[key]); + } + + beforeAll(async () => { + server = await instantiatePrism(resolve(__dirname, 'fixtures', 'invalid-examples.oas3.yaml'), { + mock: { dynamic: false, defaultExamples: true }, + }); + }); + afterAll(() => server.close()); + + describe('when running the server with defaultExamples to true', () => { + describe('and there is no preference header sent', () => { + describe('and the first schema example is invalid', () => { + let payload: unknown; + beforeAll(async () => { + payload = await fetch(new URL('/pets', server.address), { method: 'GET' }).then(r => + r.json() + ); + }); + it('should return a dynamically generated example', () => expect(hasPropertiesOfType(payload, schema)).toBe(true)); + }); + }); + + describe('and I send a request with Prefer header selecting a specific example', () => { + describe('and the selected example is invalid', () => { + let payload: unknown; + beforeAll(async () => { + payload = await fetch(new URL('/pets', server.address), { + method: 'GET', + headers: { prefer: 'example=invalid_dog' }, + }).then(r => + r.json() + ); + }); + it('should return a dynamically generated example', () => expect(hasPropertiesOfType(payload, schema)).toBe(true)); + }); + }); + }); +}); + describe.each([[...oas2File], [...oas3File]])('server %s', file => { let server: ThenArg>; beforeEach(async () => { - server = await instantiatePrism(resolve(__dirname, 'fixtures', file), { mock: { dynamic: true } }); + server = await instantiatePrism(resolve(__dirname, 'fixtures', file), { mock: { dynamic: false, defaultExamples: true } }); }); afterEach(() => server.close()); diff --git a/packages/http-server/src/server.ts b/packages/http-server/src/server.ts index 9aeda423e..ed8df7da1 100644 --- a/packages/http-server/src/server.ts +++ b/packages/http-server/src/server.ts @@ -21,6 +21,7 @@ import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; import * as E from 'fp-ts/Either'; import * as IOE from 'fp-ts/IOEither'; +import { error } from 'console'; function searchParamsToNameValues(searchParams: URLSearchParams): IHttpNameValues { const params = {}; @@ -126,14 +127,27 @@ export const createServer = (operations: IHttpOperation[], opts: IPrismHttpServe v => v.severity === DiagnosticSeverity[DiagnosticSeverity.Error] ); - if (opts.config.errors && errorViolations.length > 0) { - return IOE.left( - ProblemJsonError.fromTemplate( - VIOLATIONS, - 'Your request/response is not valid and the --errors flag is set, so Prism is generating this error for you.', - { validation: errorViolations } - ) - ); + if (errorViolations.length > 0) { + if (opts.config.errors) { + return IOE.left( + ProblemJsonError.fromTemplate( + VIOLATIONS, + 'Your request/response is not valid and the --errors flag is set, so Prism is generating this error for you.', + { validation: errorViolations } + ) + ); + } else if (opts.config.mock.defaultExamples){ + if (!response.output.defaultDynamicBody) { + return IOE.left( + ProblemJsonError.fromTemplate( + VIOLATIONS, + '`Your response was not valid and the --defaultExamples flag is set, so Prism tried to force a dynamic response, but schema is not defined.`', + { validation: errorViolations } + ) + ); + } + response.output.body = response.output.defaultDynamicBody; + } } } diff --git a/packages/http/src/mocker/index.ts b/packages/http/src/mocker/index.ts index 8c0f7060e..22d3cc96d 100644 --- a/packages/http/src/mocker/index.ts +++ b/packages/http/src/mocker/index.ts @@ -47,6 +47,7 @@ import { } from '../validator/validators/body'; import { parseMIMEHeader } from '../validator/validators/headers'; import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; +import { JSONSchema7 } from 'json-schema'; export { resetGenerator as resetJSONSchemaGenerator } from './generator/JSONSchema'; const eitherRecordSequence = Record.sequence(E.Applicative); @@ -57,8 +58,9 @@ const mock: IPrismComponents { - const payloadGenerator: PayloadGenerator = config.dynamic - ? partial(generate, resource, resource['__bundle__']) + const dynamicPayloadGenerator = partial(generate, resource, resource['__bundle__']); + const configPayloadGenerator: PayloadGenerator = config.dynamic + ? dynamicPayloadGenerator : partial(generateStatic, resource); return pipe( @@ -74,20 +76,21 @@ const mock: IPrismComponents negotiateResponse(mockConfig, input, resource)), - R.chain(result => negotiateDeprecation(result, resource)), - R.chain(result => assembleResponse(result, payloadGenerator)), + R.chain(result => (negotiateDeprecation(result, resource))), + R.chain(result => assembleResponse(result, configPayloadGenerator, dynamicPayloadGenerator)), R.chain( response => /* Note: This is now just logging the errors without propagating them back. This might be moved as a first level concept in Prism. - */ - logger => + */{ + return logger => pipe( response, E.map(mockResponseLogger(logger)), E.map(response => runCallbacks({ resource, request: input.data, response })(logger)), E.chain(() => response) ) +} ) ); }; @@ -313,7 +316,8 @@ function negotiateDeprecation( const assembleResponse = ( result: E.Either, - payloadGenerator: PayloadGenerator + configPayloadGenerator: PayloadGenerator, + dynamicPayloadGenerator: PayloadGenerator ): R.Reader> => logger => pipe( @@ -321,8 +325,8 @@ const assembleResponse = E.bind('negotiationResult', () => result), E.bind('mockedData', ({ negotiationResult }) => eitherSequence( - computeBody(negotiationResult, payloadGenerator), - computeMockedHeaders(negotiationResult.headers || [], payloadGenerator) + computeBody(negotiationResult, configPayloadGenerator), + computeMockedHeaders(negotiationResult.headers || [], configPayloadGenerator) ) ), E.map(({ mockedData: [mockedBody, mockedHeaders], negotiationResult }) => { @@ -338,6 +342,7 @@ const assembleResponse = }), }, body: mockedBody, + defaultDynamicBody: negotiationResult.schema ? dynamicBody(negotiationResult.schema, dynamicPayloadGenerator)["right"] : undefined }; logger.success(`Responding with the requested status code ${response.statusCode}`); @@ -386,11 +391,14 @@ function computeBody( return E.right(negotiationResult.bodyExample.value); } if (negotiationResult.schema) { - return pipe(payloadGenerator(negotiationResult.schema), mapPayloadGeneratorError('body')); + return dynamicBody(negotiationResult.schema, payloadGenerator); } return E.right(undefined); } +const dynamicBody = (negotiationResultSchema: JSONSchema7, payloadGenerator: PayloadGenerator, ) => + pipe(payloadGenerator(negotiationResultSchema), mapPayloadGeneratorError('body')); + const mapPayloadGeneratorError = (source: string) => E.mapLeft(err => { if (err instanceof SchemaTooComplexGeneratorError) { diff --git a/packages/http/src/mocker/negotiator/NegotiatorHelpers.ts b/packages/http/src/mocker/negotiator/NegotiatorHelpers.ts index 9621e1a84..d0ed8e493 100644 --- a/packages/http/src/mocker/negotiator/NegotiatorHelpers.ts +++ b/packages/http/src/mocker/negotiator/NegotiatorHelpers.ts @@ -49,7 +49,7 @@ const helpers = { { code, exampleKey, dynamic }: NegotiatePartialOptions, httpContent: IMediaTypeContent ): E.Either { - const { mediaType } = httpContent; + const { mediaType, schema } = httpContent; if (exampleKey) { return pipe( findExampleByKey(httpContent, exampleKey), @@ -59,11 +59,11 @@ const helpers = { `Response for contentType: ${mediaType} and exampleKey: ${exampleKey} does not exist.` ) ), - E.map(bodyExample => ({ code, mediaType, bodyExample })) + E.map(bodyExample => ({ code, mediaType, bodyExample, schema })) ); } else if (dynamic) { return pipe( - httpContent.schema, + schema, E.fromNullable(new Error(`Tried to force a dynamic response for: ${mediaType} but schema is not defined.`)), E.map(schema => ({ code, mediaType, schema })) ); @@ -71,14 +71,15 @@ const helpers = { return E.right( pipe( findFirstExample(httpContent), - O.map(bodyExample => ({ code, mediaType, bodyExample })), - O.alt(() => - pipe( - O.fromNullable(httpContent.schema), + O.map(bodyExample => ({ code, mediaType, bodyExample, schema })), + O.alt(() => { + return pipe( + O.fromNullable(schema), O.map(schema => ({ schema, code, mediaType })) ) + } ), - O.getOrElse(() => ({ code, mediaType })) + O.getOrElse(() => ({ code, mediaType, schema })) ) ); } diff --git a/packages/http/src/types.ts b/packages/http/src/types.ts index 0019fcc03..aa843b98e 100644 --- a/packages/http/src/types.ts +++ b/packages/http/src/types.ts @@ -13,6 +13,7 @@ export interface IHttpOperationConfig { code?: number; exampleKey?: string; dynamic: boolean; + defaultExamples?: boolean; } export type IHttpMockConfig = Overwrite; @@ -40,6 +41,7 @@ export interface IHttpResponse { statusCode: number; headers?: IHttpNameValue; body?: unknown; + defaultDynamicBody?: unknown; } export type ProblemJson = { From e3e1a092bdd42585a5da896b342993aeffd0ffa0 Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Tue, 12 Sep 2023 12:46:09 -0700 Subject: [PATCH 02/11] revert faulty master commit --- packages/cli/src/commands/mock.ts | 9 +--- packages/cli/src/commands/proxy.ts | 1 - packages/cli/src/util/createServer.ts | 8 +-- .../fixtures/invalid-examples.oas3.yaml | 48 ----------------- .../src/__tests__/server.oas.spec.ts | 52 +------------------ packages/http-server/src/server.ts | 30 +++-------- packages/http/src/mocker/index.ts | 28 ++++------ .../mocker/negotiator/NegotiatorHelpers.ts | 17 +++--- packages/http/src/types.ts | 2 - 9 files changed, 32 insertions(+), 163 deletions(-) delete mode 100644 packages/http-server/src/__tests__/fixtures/invalid-examples.oas3.yaml diff --git a/packages/cli/src/commands/mock.ts b/packages/cli/src/commands/mock.ts index f9a50358a..d6fc6ac44 100644 --- a/packages/cli/src/commands/mock.ts +++ b/packages/cli/src/commands/mock.ts @@ -25,16 +25,10 @@ const mockCommand: CommandModule = { default: undefined, boolean: true, }, - defaultExamples: { - alias: 'de', - description: 'Generates dynamic default response examples in place of schema examples that are invalid.', - boolean: true, - default: false, - }, }), handler: parsedArgs => { parsedArgs.jsonSchemaFakerFillProperties = parsedArgs['json-schema-faker-fillProperties']; - const { multiprocess, dynamic, port, host, cors, document, errors, verboseLevel, defaultExamples, jsonSchemaFakerFillProperties } = + const { multiprocess, dynamic, port, host, cors, document, errors, verboseLevel, jsonSchemaFakerFillProperties } = parsedArgs as unknown as CreateMockServerOptions; const createPrism = multiprocess ? createMultiProcessPrism : createSingleProcessPrism; @@ -47,7 +41,6 @@ const mockCommand: CommandModule = { multiprocess, errors, verboseLevel, - defaultExamples, jsonSchemaFakerFillProperties, }; diff --git a/packages/cli/src/commands/proxy.ts b/packages/cli/src/commands/proxy.ts index da5ddf0ae..a806d3db8 100644 --- a/packages/cli/src/commands/proxy.ts +++ b/packages/cli/src/commands/proxy.ts @@ -51,7 +51,6 @@ const proxyCommand: CommandModule = { 'errors', 'validateRequest', 'verboseLevel', - 'defaultExamples', 'upstreamProxy', 'jsonSchemaFakerFillProperties' ); diff --git a/packages/cli/src/util/createServer.ts b/packages/cli/src/util/createServer.ts index e9feb9db4..11f1231c2 100644 --- a/packages/cli/src/util/createServer.ts +++ b/packages/cli/src/util/createServer.ts @@ -85,9 +85,9 @@ async function createPrismServerWithLogger(options: CreateBaseServerOptions, log validateRequest, validateResponse: true, checkSecurity: true, - errors: options.errors, + errors: false, upstreamProxy: undefined, - mock: { dynamic: options.dynamic, defaultExamples: options.defaultExamples }, + mock: { dynamic: options.dynamic }, }; const config: IHttpConfig = isProxyServerOptions(options) @@ -95,9 +95,10 @@ async function createPrismServerWithLogger(options: CreateBaseServerOptions, log ...shared, isProxy: true, upstream: options.upstream, + errors: options.errors, upstreamProxy: options.upstreamProxy, } - : { ...shared, isProxy: false }; + : { ...shared, isProxy: false, errors: options.errors }; const server = createHttpServer(operations, { cors: options.cors, @@ -154,7 +155,6 @@ type CreateBaseServerOptions = { multiprocess: boolean; errors: boolean; verboseLevel: string; - defaultExamples: boolean; jsonSchemaFakerFillProperties: boolean; }; diff --git a/packages/http-server/src/__tests__/fixtures/invalid-examples.oas3.yaml b/packages/http-server/src/__tests__/fixtures/invalid-examples.oas3.yaml deleted file mode 100644 index eaf89a8ee..000000000 --- a/packages/http-server/src/__tests__/fixtures/invalid-examples.oas3.yaml +++ /dev/null @@ -1,48 +0,0 @@ -openapi: 3.0.2 -paths: - /pets: - get: - responses: - '200': - description: OK - content: - application/json: - schema: - '$ref': '#/schemas/Pet' - examples: - invalid_cat: - summary: An invalid example of a cat - value: - id: 135 - status: "new" - invalid_dog: - summary: An invalid example of a dog - value: - id: 135 - name: 123 - valid_dog: - summary: A valid example of a dog - value: - id: 135 - name: "dog" - name: "available" - -schemas: - Pet: - type: object - required: - - name - properties: - id: - type: integer - format: int64 - name: - type: string - example: doggie - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold \ No newline at end of file diff --git a/packages/http-server/src/__tests__/server.oas.spec.ts b/packages/http-server/src/__tests__/server.oas.spec.ts index c70be7a8e..95a4e7a7b 100644 --- a/packages/http-server/src/__tests__/server.oas.spec.ts +++ b/packages/http-server/src/__tests__/server.oas.spec.ts @@ -6,7 +6,6 @@ import { merge } from 'lodash'; import fetch, { RequestInit } from 'node-fetch'; import { createServer } from '../'; import { ThenArg } from '../types'; -import { WithUnknownContainers } from 'io-ts/lib/Schemable'; const logger = createLogger('TEST', { enabled: false }); @@ -137,60 +136,11 @@ describe('Prefer header overrides', () => { }); }); -describe('Invalid examples', () => { - let server: ThenArg>; - const schema = { id: 'number', name: 'string', status: 'string' }; - - interface PayloadSchema { - [key: string]: string; - } - - function hasPropertiesOfType(obj: any, schema: PayloadSchema): boolean { - return Object.keys(schema).every(key => typeof obj[key] === schema[key]); - } - - beforeAll(async () => { - server = await instantiatePrism(resolve(__dirname, 'fixtures', 'invalid-examples.oas3.yaml'), { - mock: { dynamic: false, defaultExamples: true }, - }); - }); - afterAll(() => server.close()); - - describe('when running the server with defaultExamples to true', () => { - describe('and there is no preference header sent', () => { - describe('and the first schema example is invalid', () => { - let payload: unknown; - beforeAll(async () => { - payload = await fetch(new URL('/pets', server.address), { method: 'GET' }).then(r => - r.json() - ); - }); - it('should return a dynamically generated example', () => expect(hasPropertiesOfType(payload, schema)).toBe(true)); - }); - }); - - describe('and I send a request with Prefer header selecting a specific example', () => { - describe('and the selected example is invalid', () => { - let payload: unknown; - beforeAll(async () => { - payload = await fetch(new URL('/pets', server.address), { - method: 'GET', - headers: { prefer: 'example=invalid_dog' }, - }).then(r => - r.json() - ); - }); - it('should return a dynamically generated example', () => expect(hasPropertiesOfType(payload, schema)).toBe(true)); - }); - }); - }); -}); - describe.each([[...oas2File], [...oas3File]])('server %s', file => { let server: ThenArg>; beforeEach(async () => { - server = await instantiatePrism(resolve(__dirname, 'fixtures', file), { mock: { dynamic: false, defaultExamples: true } }); + server = await instantiatePrism(resolve(__dirname, 'fixtures', file), { mock: { dynamic: true } }); }); afterEach(() => server.close()); diff --git a/packages/http-server/src/server.ts b/packages/http-server/src/server.ts index ed8df7da1..9aeda423e 100644 --- a/packages/http-server/src/server.ts +++ b/packages/http-server/src/server.ts @@ -21,7 +21,6 @@ import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; import * as E from 'fp-ts/Either'; import * as IOE from 'fp-ts/IOEither'; -import { error } from 'console'; function searchParamsToNameValues(searchParams: URLSearchParams): IHttpNameValues { const params = {}; @@ -127,27 +126,14 @@ export const createServer = (operations: IHttpOperation[], opts: IPrismHttpServe v => v.severity === DiagnosticSeverity[DiagnosticSeverity.Error] ); - if (errorViolations.length > 0) { - if (opts.config.errors) { - return IOE.left( - ProblemJsonError.fromTemplate( - VIOLATIONS, - 'Your request/response is not valid and the --errors flag is set, so Prism is generating this error for you.', - { validation: errorViolations } - ) - ); - } else if (opts.config.mock.defaultExamples){ - if (!response.output.defaultDynamicBody) { - return IOE.left( - ProblemJsonError.fromTemplate( - VIOLATIONS, - '`Your response was not valid and the --defaultExamples flag is set, so Prism tried to force a dynamic response, but schema is not defined.`', - { validation: errorViolations } - ) - ); - } - response.output.body = response.output.defaultDynamicBody; - } + if (opts.config.errors && errorViolations.length > 0) { + return IOE.left( + ProblemJsonError.fromTemplate( + VIOLATIONS, + 'Your request/response is not valid and the --errors flag is set, so Prism is generating this error for you.', + { validation: errorViolations } + ) + ); } } diff --git a/packages/http/src/mocker/index.ts b/packages/http/src/mocker/index.ts index 22d3cc96d..8c0f7060e 100644 --- a/packages/http/src/mocker/index.ts +++ b/packages/http/src/mocker/index.ts @@ -47,7 +47,6 @@ import { } from '../validator/validators/body'; import { parseMIMEHeader } from '../validator/validators/headers'; import { NonEmptyArray } from 'fp-ts/NonEmptyArray'; -import { JSONSchema7 } from 'json-schema'; export { resetGenerator as resetJSONSchemaGenerator } from './generator/JSONSchema'; const eitherRecordSequence = Record.sequence(E.Applicative); @@ -58,9 +57,8 @@ const mock: IPrismComponents { - const dynamicPayloadGenerator = partial(generate, resource, resource['__bundle__']); - const configPayloadGenerator: PayloadGenerator = config.dynamic - ? dynamicPayloadGenerator + const payloadGenerator: PayloadGenerator = config.dynamic + ? partial(generate, resource, resource['__bundle__']) : partial(generateStatic, resource); return pipe( @@ -76,21 +74,20 @@ const mock: IPrismComponents negotiateResponse(mockConfig, input, resource)), - R.chain(result => (negotiateDeprecation(result, resource))), - R.chain(result => assembleResponse(result, configPayloadGenerator, dynamicPayloadGenerator)), + R.chain(result => negotiateDeprecation(result, resource)), + R.chain(result => assembleResponse(result, payloadGenerator)), R.chain( response => /* Note: This is now just logging the errors without propagating them back. This might be moved as a first level concept in Prism. - */{ - return logger => + */ + logger => pipe( response, E.map(mockResponseLogger(logger)), E.map(response => runCallbacks({ resource, request: input.data, response })(logger)), E.chain(() => response) ) -} ) ); }; @@ -316,8 +313,7 @@ function negotiateDeprecation( const assembleResponse = ( result: E.Either, - configPayloadGenerator: PayloadGenerator, - dynamicPayloadGenerator: PayloadGenerator + payloadGenerator: PayloadGenerator ): R.Reader> => logger => pipe( @@ -325,8 +321,8 @@ const assembleResponse = E.bind('negotiationResult', () => result), E.bind('mockedData', ({ negotiationResult }) => eitherSequence( - computeBody(negotiationResult, configPayloadGenerator), - computeMockedHeaders(negotiationResult.headers || [], configPayloadGenerator) + computeBody(negotiationResult, payloadGenerator), + computeMockedHeaders(negotiationResult.headers || [], payloadGenerator) ) ), E.map(({ mockedData: [mockedBody, mockedHeaders], negotiationResult }) => { @@ -342,7 +338,6 @@ const assembleResponse = }), }, body: mockedBody, - defaultDynamicBody: negotiationResult.schema ? dynamicBody(negotiationResult.schema, dynamicPayloadGenerator)["right"] : undefined }; logger.success(`Responding with the requested status code ${response.statusCode}`); @@ -391,14 +386,11 @@ function computeBody( return E.right(negotiationResult.bodyExample.value); } if (negotiationResult.schema) { - return dynamicBody(negotiationResult.schema, payloadGenerator); + return pipe(payloadGenerator(negotiationResult.schema), mapPayloadGeneratorError('body')); } return E.right(undefined); } -const dynamicBody = (negotiationResultSchema: JSONSchema7, payloadGenerator: PayloadGenerator, ) => - pipe(payloadGenerator(negotiationResultSchema), mapPayloadGeneratorError('body')); - const mapPayloadGeneratorError = (source: string) => E.mapLeft(err => { if (err instanceof SchemaTooComplexGeneratorError) { diff --git a/packages/http/src/mocker/negotiator/NegotiatorHelpers.ts b/packages/http/src/mocker/negotiator/NegotiatorHelpers.ts index d0ed8e493..9621e1a84 100644 --- a/packages/http/src/mocker/negotiator/NegotiatorHelpers.ts +++ b/packages/http/src/mocker/negotiator/NegotiatorHelpers.ts @@ -49,7 +49,7 @@ const helpers = { { code, exampleKey, dynamic }: NegotiatePartialOptions, httpContent: IMediaTypeContent ): E.Either { - const { mediaType, schema } = httpContent; + const { mediaType } = httpContent; if (exampleKey) { return pipe( findExampleByKey(httpContent, exampleKey), @@ -59,11 +59,11 @@ const helpers = { `Response for contentType: ${mediaType} and exampleKey: ${exampleKey} does not exist.` ) ), - E.map(bodyExample => ({ code, mediaType, bodyExample, schema })) + E.map(bodyExample => ({ code, mediaType, bodyExample })) ); } else if (dynamic) { return pipe( - schema, + httpContent.schema, E.fromNullable(new Error(`Tried to force a dynamic response for: ${mediaType} but schema is not defined.`)), E.map(schema => ({ code, mediaType, schema })) ); @@ -71,15 +71,14 @@ const helpers = { return E.right( pipe( findFirstExample(httpContent), - O.map(bodyExample => ({ code, mediaType, bodyExample, schema })), - O.alt(() => { - return pipe( - O.fromNullable(schema), + O.map(bodyExample => ({ code, mediaType, bodyExample })), + O.alt(() => + pipe( + O.fromNullable(httpContent.schema), O.map(schema => ({ schema, code, mediaType })) ) - } ), - O.getOrElse(() => ({ code, mediaType, schema })) + O.getOrElse(() => ({ code, mediaType })) ) ); } diff --git a/packages/http/src/types.ts b/packages/http/src/types.ts index aa843b98e..0019fcc03 100644 --- a/packages/http/src/types.ts +++ b/packages/http/src/types.ts @@ -13,7 +13,6 @@ export interface IHttpOperationConfig { code?: number; exampleKey?: string; dynamic: boolean; - defaultExamples?: boolean; } export type IHttpMockConfig = Overwrite; @@ -41,7 +40,6 @@ export interface IHttpResponse { statusCode: number; headers?: IHttpNameValue; body?: unknown; - defaultDynamicBody?: unknown; } export type ProblemJson = { From 3a9dc491e838a1c1d84220516f6e947dad1156a4 Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Sun, 14 Apr 2024 23:58:01 -0700 Subject: [PATCH 03/11] Add unit test for readonly objects in arrays (currently fails as expected) --- .../filterRequiredProperties.test.ts | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts index 9cb0f93c6..49514b6b3 100644 --- a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts +++ b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts @@ -23,7 +23,7 @@ describe('filterRequiredProperties', () => { }); }); - it('strips readOnly properties', () => { + it('strips readOnly properties on standalone object', () => { const schema: JSONSchema = { type: 'object', properties: { @@ -43,6 +43,44 @@ describe('filterRequiredProperties', () => { }); }); + it('strips readOnly properties from objects within an array', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + objectsArray: { + type: 'array', + items: { + type: 'object', + required: ['id', 'name'], + properties: { + id: { + readOnly: true, + type: 'string' + }, + name: { + type: 'string' + } + } + } + } + } + }; + + assertSome(stripReadOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + const arr_items = (schema.properties.objectsArray as JSONSchema).items as JSONSchema + expect(arr_items).not.toBeNull() + if (arr_items){ + expect(arr_items.required).toEqual(['name']); + expect(arr_items.properties).toEqual({ + name: expect.any(Object), + }); + } + } + }); + }); + it('strips nested writeOnly properties', () => { const schema: JSONSchema = { type: 'object', From ab77381208f689c9912c98c5c70e55287d80298c Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Tue, 16 Apr 2024 14:58:40 -0700 Subject: [PATCH 04/11] get messy version working for non-tuple typed arrays --- .../filterRequiredProperties.test.ts | 135 +++++++++++++- .../src/utils/filterRequiredProperties.ts | 169 ++++++++++++++---- 2 files changed, 268 insertions(+), 36 deletions(-) diff --git a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts index 49514b6b3..5d327583c 100644 --- a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts +++ b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts @@ -43,7 +43,7 @@ describe('filterRequiredProperties', () => { }); }); - it('strips readOnly properties from objects within an array', () => { + it('strips readOnly properties from objects in single schema array', () => { const schema: JSONSchema = { type: 'object', properties: { @@ -62,16 +62,109 @@ describe('filterRequiredProperties', () => { } } } + }, + title: { type: 'string', readOnly: true }, + address: { type: 'integer' }, + }, + required: ['title', 'address'], + }; + + assertSome(stripReadOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + console.log("FINAL", schema.properties) + const arr_items = (schema.properties.objectsArray as JSONSchema).items as JSONSchema + console.log("FINAL ARR", arr_items) + expect(arr_items).not.toBeNull() + if (arr_items){ + expect(arr_items.required).toEqual(['name']); + expect(arr_items.properties).toEqual({ + name: expect.any(Object), + }); + expect(schema.required).toEqual(['address']); + expect(schema.properties).toEqual({ + address: expect.any(Object), + objectsArray: expect.any(Object), + }); + } + } + }); + }); + + it('strips readOnly properties from objects in tuple-typed array and unspecified additionalItems', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + objectsArrayAdditionalItemsUnspecified: { + type: 'array', + items: [ + { + type: 'object', + required: ['id', 'name'], + properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + }, + { + type: 'object', + required: ['id', 'name'], + properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + } + ] + }, + } + }; + + // assertSome(stripReadOnlyProperties(schema), schema => { + // expect(schema.properties).not.toBeNull() + // if (schema.properties) { + // const arr_items = (schema.properties.objectsArrayAdditionalItemsUnspecified as JSONSchema).items as JSONSchema + // expect(arr_items).not.toBeNull() + // if (arr_items){ + // expect(arr_items.required).toEqual(['name']); + // expect(arr_items.properties).toEqual({ + // name: expect.any(Object), + // }); + // } + // } + // }); + }); + + it('strips readOnly properties from objects in tuple-typed array with additionalItems', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + objectsArrayWithAdditionalItems: { + type: 'array', + items: [ + { + type: 'object', + required: ['id', 'name'], + properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + }, + { + type: 'object', + required: ['id', 'name'], + properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + } + ], + additionalItems: { + type: 'object', + properties: { + status: { type: 'string' } + }, + required: ['status'] + } } } }; assertSome(stripReadOnlyProperties(schema), schema => { + console.log("RESULT", schema.properties) expect(schema.properties).not.toBeNull() if (schema.properties) { - const arr_items = (schema.properties.objectsArray as JSONSchema).items as JSONSchema + const arr_items = (schema.properties.objectsArrayWithAdditionalItems as JSONSchema).items as JSONSchema expect(arr_items).not.toBeNull() if (arr_items){ + console.log("RESULT2", arr_items) expect(arr_items.required).toEqual(['name']); expect(arr_items.properties).toEqual({ name: expect.any(Object), @@ -81,6 +174,44 @@ describe('filterRequiredProperties', () => { }); }); + it('strips readOnly properties from objects within tuple-typed array no additionalItems', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + objectsArrayNoAdditionalItems: { + type: 'array', + items: [ + { + type: 'object', + required: ['id', 'name'], + properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + }, + { + type: 'object', + required: ['id', 'name'], + properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + } + ], + additionalItems: false + }, + } + }; + + // assertSome(stripReadOnlyProperties(schema), schema => { + // expect(schema.properties).not.toBeNull() + // if (schema.properties) { + // const arr_items = (schema.properties.objectsArrayNoAdditionalItems as JSONSchema).items as JSONSchema + // expect(arr_items).not.toBeNull() + // if (arr_items){ + // expect(arr_items.required).toEqual(['name']); + // expect(arr_items.properties).toEqual({ + // name: expect.any(Object), + // }); + // } + // } + // }); + }); + it('strips nested writeOnly properties', () => { const schema: JSONSchema = { type: 'object', diff --git a/packages/http/src/utils/filterRequiredProperties.ts b/packages/http/src/utils/filterRequiredProperties.ts index 5b3fd11d8..41df1221a 100644 --- a/packages/http/src/utils/filterRequiredProperties.ts +++ b/packages/http/src/utils/filterRequiredProperties.ts @@ -5,22 +5,144 @@ import { Option } from 'fp-ts/Option'; import * as A from 'fp-ts/Array'; import { JSONSchema } from '../types'; -type Properties = Record; +type JSONSchemaObjectType = JSONSchema6 | JSONSchema7 | JSONSchema4 +type JSONSchemaArrType = JSONSchema4[] | JSONSchema6[] | JSONSchema7[] + +type Properties = Record; +type ArrayItems = JSONSchemaObjectType | JSONSchemaArrType | boolean; +type ArrayAdditionalItems = JSONSchemaObjectType | boolean; type RequiredSchemaSubset = { readOnly?: boolean; writeOnly?: boolean; properties?: Properties; required?: string[] | false; + items?: ArrayItems; + additionalItems?: ArrayAdditionalItems; }; const buildSchemaFilter = ( keepPropertyPredicate: (schema: S) => Option ): ((schema: S) => Option) => { function filterProperties(schema: S): Option { + console.log("SCHEMA ITEMS", schema.items) + return pipe( + O.fromNullable(schema.items), + O.chain(items => { + if (typeof items === 'object') { + console.log("ARRAY FOUND PROPS") + return pipe( + O.fromNullable((items as JSONSchemaObjectType).properties), + O.chainNullableK(properties => properties as Properties) + ) + } + // else if (Array.isArray(items)) { + // O.map(items => + // pipe( + // items, + // A.map(item => ) + // ) + // ) + // } + return O.none + }), + O.alt(() => { + console.log("ALT PROPS") + return O.fromNullable(schema.properties)}), + (unfilteredProps) => filterPropertiesHelper(unfilteredProps), + O.map(filteredProperties => { + console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema) + if (schema.items && typeof schema.items === 'object') { + return { + ...schema, + items: { + ...schema.items, + properties: filteredProperties, + }, + }; + } + return { + ...schema, + properties: filteredProperties, + } + } + ), + O.alt(() => { + console.log("RESULT SCHEMA", schema) + return O.some(schema) + }) + ); + } + + function filterRequired(updatedSchema: S, originalSchema: S): Option { + return pipe( + updatedSchema, + O.fromPredicate((schema: S) => { + let required; + if (schema.items && typeof schema.items === 'object') { + required = (schema.items as JSONSchemaObjectType).required; + } else { + required = schema.required; + } + return Array.isArray(required) + }), + O.map(schema => + { + let properties; + if (schema.items && typeof schema.items === 'object') { + properties = (schema.items as JSONSchemaObjectType).properties; + } else { + properties = schema.properties; + } + return Object.keys(properties || {}) + } + ), + O.map(updatedProperties => { + let properties; + if (originalSchema.items && typeof originalSchema.items === 'object') { + properties = (originalSchema.items as JSONSchemaObjectType).properties; + } else { + properties = originalSchema.properties; + } + + const originalPropertyNames = Object.keys(properties || {}); + return originalPropertyNames.filter(name => !updatedProperties.includes(name)); + }), + O.map(removedProperties => + { + let required; + if (originalSchema.items && typeof originalSchema.items === 'object') { + required = (originalSchema.items as JSONSchemaObjectType).required; + } else { + required = originalSchema.required; + } + return (required as string[]).filter(name => !removedProperties.includes(name)) + } + ), + O.map(required => { + console.log("UPDATED SCHEMA", updatedSchema, "REQUIRED", required) + if (updatedSchema.items && typeof updatedSchema.items === 'object') { + return { + ...updatedSchema, + items: { + ...updatedSchema.items, + required: required, + }, + }; + } + return { + ...updatedSchema, + required: required, + } + }), + O.alt(() => O.some(updatedSchema)) + ); + } + + function filterPropertiesHelper(properties: Option): Option { return pipe( - O.fromNullable(schema.properties), - O.map(properties => + properties, + O.map(properties => pipe( Object.keys(properties), A.reduce( @@ -29,10 +151,13 @@ const buildSchemaFilter = ( return pipe( properties[propertyName], O.fromPredicate(p => { - if (typeof p === 'boolean') { + console.log("properties", properties) + console.log("propertyName", propertyName) + if (typeof p === 'boolean') { // I think this is bc Object.keys only handles string props so boolean props need to be added back in manually filteredProperties[propertyName] = properties[propertyName]; return false; } + console.log("P", p) return true; }), O.chain(p => filter(p as S)), @@ -45,42 +170,18 @@ const buildSchemaFilter = ( } ) ) - ), - O.map(filteredProperties => ({ - ...schema, - properties: filteredProperties, - })), - O.alt(() => O.some(schema)) - ); - } - - function filterRequired(updatedSchema: S, originalSchema: S): Option { - return pipe( - updatedSchema, - O.fromPredicate((schema: S) => Array.isArray(schema.required)), - O.map(schema => Object.keys(schema.properties || {})), - O.map(updatedProperties => { - const originalPropertyNames = Object.keys(originalSchema.properties || {}); - return originalPropertyNames.filter(name => !updatedProperties.includes(name)); - }), - O.map(removedProperties => - (originalSchema.required as string[]).filter(name => !removedProperties.includes(name)) - ), - O.map(required => { - return { - ...updatedSchema, - required, - }; - }), - O.alt(() => O.some(updatedSchema)) - ); + ) + ) } function filter(inputSchema: S): Option { return pipe( inputSchema, keepPropertyPredicate, - O.chain(inputSchema => filterProperties(inputSchema)), + O.chain(inputSchema => { + console.log("INPUT SCHEMA", inputSchema) + return filterProperties(inputSchema) + } ), O.chain(schema => filterRequired(schema, inputSchema)) ); } From b99c895c9b159246347b8a4c622b29971826d5ce Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Wed, 17 Apr 2024 00:48:39 -0700 Subject: [PATCH 05/11] get tuple typed arrays properties (NOT required) working, additionoalItems NOT accounted for yet --- .../filterRequiredProperties.test.ts | 35 +++++---- .../src/utils/filterRequiredProperties.ts | 72 +++++++++++++++---- 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts index 5d327583c..35a2d66b8 100644 --- a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts +++ b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts @@ -105,27 +105,32 @@ describe('filterRequiredProperties', () => { }, { type: 'object', - required: ['id', 'name'], - properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + required: ['address', 'title'], + properties: { address: { readOnly: true, type: 'string' }, title: { type: 'string' } } } ] }, } }; - // assertSome(stripReadOnlyProperties(schema), schema => { - // expect(schema.properties).not.toBeNull() - // if (schema.properties) { - // const arr_items = (schema.properties.objectsArrayAdditionalItemsUnspecified as JSONSchema).items as JSONSchema - // expect(arr_items).not.toBeNull() - // if (arr_items){ - // expect(arr_items.required).toEqual(['name']); - // expect(arr_items.properties).toEqual({ - // name: expect.any(Object), - // }); - // } - // } - // }); + assertSome(stripReadOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + console.log("FINAL", schema.properties) + const arr_items = (schema.properties.objectsArrayAdditionalItemsUnspecified as JSONSchema).items as JSONSchema + console.log("FINAL ARR", arr_items) + expect(arr_items).not.toBeNull() + if (arr_items){ + // expect(arr_items.required).toEqual(['name']); + expect(arr_items[0].properties).toEqual({ + name: expect.any(Object), + }); + expect(arr_items[1].properties).toEqual({ + title: expect.any(Object), + }); + } + } + }); }); it('strips readOnly properties from objects in tuple-typed array with additionalItems', () => { diff --git a/packages/http/src/utils/filterRequiredProperties.ts b/packages/http/src/utils/filterRequiredProperties.ts index 41df1221a..1b6a95aa9 100644 --- a/packages/http/src/utils/filterRequiredProperties.ts +++ b/packages/http/src/utils/filterRequiredProperties.ts @@ -24,7 +24,7 @@ type RequiredSchemaSubset = { const buildSchemaFilter = ( keepPropertyPredicate: (schema: S) => Option ): ((schema: S) => Option) => { - function filterProperties(schema: S): Option { + function filterNonTupleTypedProperties(schema: S): Option { console.log("SCHEMA ITEMS", schema.items) return pipe( O.fromNullable(schema.items), @@ -36,20 +36,12 @@ const buildSchemaFilter = ( O.chainNullableK(properties => properties as Properties) ) } - // else if (Array.isArray(items)) { - // O.map(items => - // pipe( - // items, - // A.map(item => ) - // ) - // ) - // } return O.none }), O.alt(() => { - console.log("ALT PROPS") + console.log("NORMAL NON ARRAY PROPS") return O.fromNullable(schema.properties)}), - (unfilteredProps) => filterPropertiesHelper(unfilteredProps), + O.chain(unfilteredProps => filterPropertiesHelper(O.fromNullable(unfilteredProps as Properties))), O.map(filteredProperties => { console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema) if (schema.items && typeof schema.items === 'object') { @@ -74,6 +66,55 @@ const buildSchemaFilter = ( ); } + function filterTupleTypedProperties(schema: S): Option { + console.log("SCHEMA ITEMS", schema.items) + return pipe( + O.fromNullable(schema.items as Properties[]), + O.chain(items => { + return pipe( + items, + A.map(item => (item as JSONSchemaObjectType).properties), + propertiesArray => O.fromNullable(propertiesArray as Properties[]) // Casting because Properties is likely expected to be an object, not an array + ) + } + ), + O.map(unfilteredProps => { + return pipe( + unfilteredProps, + A.map(unfilteredProp => filterPropertiesHelper(O.fromNullable(unfilteredProp as Properties))), + ) + } + ), + O.chain(filteredProperties => { + console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema) + if (schema.items && typeof schema.items === 'object') { + const items = (schema.items as JSONSchemaArrType).map((item, index) => { + const defaultProperty = {}; + const properties = + pipe( + filteredProperties[index], + O.getOrElse(() => defaultProperty) + ); + return { ...item, properties }; + }); + + return O.some({ + ...schema, + items: { + ...items + }, + }); + } + return O.none + } + ), + O.alt(() => { + console.log("RESULT SCHEMA", schema) + return O.some(schema) + }) + ); + } + function filterRequired(updatedSchema: S, originalSchema: S): Option { return pipe( updatedSchema, @@ -141,8 +182,8 @@ const buildSchemaFilter = ( function filterPropertiesHelper(properties: Option): Option { return pipe( - properties, - O.map(properties => + properties, + O.map(properties => pipe( Object.keys(properties), A.reduce( @@ -180,7 +221,10 @@ const buildSchemaFilter = ( keepPropertyPredicate, O.chain(inputSchema => { console.log("INPUT SCHEMA", inputSchema) - return filterProperties(inputSchema) + if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing + return filterTupleTypedProperties(inputSchema) + } + return filterNonTupleTypedProperties(inputSchema) } ), O.chain(schema => filterRequired(schema, inputSchema)) ); From 957ab3772cc9499f4e5d86833a41b4c1eda1ce36 Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Wed, 17 Apr 2024 01:04:05 -0700 Subject: [PATCH 06/11] clean up filterNonTupleTypedRequired --- .../src/utils/filterRequiredProperties.ts | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/packages/http/src/utils/filterRequiredProperties.ts b/packages/http/src/utils/filterRequiredProperties.ts index 1b6a95aa9..cf05235b9 100644 --- a/packages/http/src/utils/filterRequiredProperties.ts +++ b/packages/http/src/utils/filterRequiredProperties.ts @@ -4,6 +4,7 @@ import * as O from 'fp-ts/Option'; import { Option } from 'fp-ts/Option'; import * as A from 'fp-ts/Array'; import { JSONSchema } from '../types'; +import { update } from 'lodash'; type JSONSchemaObjectType = JSONSchema6 | JSONSchema7 | JSONSchema4 type JSONSchemaArrType = JSONSchema4[] | JSONSchema6[] | JSONSchema7[] @@ -115,51 +116,26 @@ const buildSchemaFilter = ( ); } - function filterRequired(updatedSchema: S, originalSchema: S): Option { + function filterNonTupleTypedRequired(updatedSchema: S, originalSchema: S): Option { + function getCorrectSchema(schema: S) { + if (schema.items && typeof schema.items === 'object') { // i.e. this is an array + return (schema.items as JSONSchemaObjectType); + } + return schema; // i.e. this is an object + } + return pipe( updatedSchema, - O.fromPredicate((schema: S) => { - let required; - if (schema.items && typeof schema.items === 'object') { - required = (schema.items as JSONSchemaObjectType).required; - } else { - required = schema.required; - } - return Array.isArray(required) - }), - O.map(schema => - { - let properties; - if (schema.items && typeof schema.items === 'object') { - properties = (schema.items as JSONSchemaObjectType).properties; - } else { - properties = schema.properties; - } - return Object.keys(properties || {}) - } - ), + O.fromPredicate(schema => Array.isArray(getCorrectSchema(schema).required)), + O.map(schema => Object.keys(getCorrectSchema(schema).properties || {})), O.map(updatedProperties => { - let properties; - if (originalSchema.items && typeof originalSchema.items === 'object') { - properties = (originalSchema.items as JSONSchemaObjectType).properties; - } else { - properties = originalSchema.properties; - } - - const originalPropertyNames = Object.keys(properties || {}); + const originalPropertyNames = Object.keys(getCorrectSchema(originalSchema).properties || {}); return originalPropertyNames.filter(name => !updatedProperties.includes(name)); }), - O.map(removedProperties => - { - let required; - if (originalSchema.items && typeof originalSchema.items === 'object') { - required = (originalSchema.items as JSONSchemaObjectType).required; - } else { - required = originalSchema.required; - } + O.map(removedProperties => { + const required = getCorrectSchema(originalSchema).required return (required as string[]).filter(name => !removedProperties.includes(name)) - } - ), + }), O.map(required => { console.log("UPDATED SCHEMA", updatedSchema, "REQUIRED", required) if (updatedSchema.items && typeof updatedSchema.items === 'object') { @@ -226,7 +202,7 @@ const buildSchemaFilter = ( } return filterNonTupleTypedProperties(inputSchema) } ), - O.chain(schema => filterRequired(schema, inputSchema)) + O.chain(schema => filterNonTupleTypedRequired(schema, inputSchema)) ); } From 0cafb75644340e974154b3eef54a4958318522f2 Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Wed, 17 Apr 2024 20:07:45 -0700 Subject: [PATCH 07/11] messy working version for tuple-typed arrays (properties AND required), additionalItems not implemented yet --- .../filterRequiredProperties.test.ts | 33 +-- .../src/utils/filterRequiredProperties.ts | 210 +++++++++++------- 2 files changed, 147 insertions(+), 96 deletions(-) diff --git a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts index 35a2d66b8..a5f222b1c 100644 --- a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts +++ b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts @@ -121,10 +121,11 @@ describe('filterRequiredProperties', () => { console.log("FINAL ARR", arr_items) expect(arr_items).not.toBeNull() if (arr_items){ - // expect(arr_items.required).toEqual(['name']); + expect(arr_items[0].required).toEqual(['name']); expect(arr_items[0].properties).toEqual({ name: expect.any(Object), }); + expect(arr_items[1].required).toEqual(['title']); expect(arr_items[1].properties).toEqual({ title: expect.any(Object), }); @@ -162,21 +163,21 @@ describe('filterRequiredProperties', () => { } }; - assertSome(stripReadOnlyProperties(schema), schema => { - console.log("RESULT", schema.properties) - expect(schema.properties).not.toBeNull() - if (schema.properties) { - const arr_items = (schema.properties.objectsArrayWithAdditionalItems as JSONSchema).items as JSONSchema - expect(arr_items).not.toBeNull() - if (arr_items){ - console.log("RESULT2", arr_items) - expect(arr_items.required).toEqual(['name']); - expect(arr_items.properties).toEqual({ - name: expect.any(Object), - }); - } - } - }); + // assertSome(stripReadOnlyProperties(schema), schema => { + // console.log("RESULT", schema.properties) + // expect(schema.properties).not.toBeNull() + // if (schema.properties) { + // const arr_items = (schema.properties.objectsArrayWithAdditionalItems as JSONSchema).items as JSONSchema + // expect(arr_items).not.toBeNull() + // if (arr_items){ + // console.log("RESULT2", arr_items) + // expect(arr_items.required).toEqual(['name']); + // expect(arr_items.properties).toEqual({ + // name: expect.any(Object), + // }); + // } + // } + // }); }); it('strips readOnly properties from objects within tuple-typed array no additionalItems', () => { diff --git a/packages/http/src/utils/filterRequiredProperties.ts b/packages/http/src/utils/filterRequiredProperties.ts index cf05235b9..32ae06dc9 100644 --- a/packages/http/src/utils/filterRequiredProperties.ts +++ b/packages/http/src/utils/filterRequiredProperties.ts @@ -7,7 +7,7 @@ import { JSONSchema } from '../types'; import { update } from 'lodash'; type JSONSchemaObjectType = JSONSchema6 | JSONSchema7 | JSONSchema4 -type JSONSchemaArrType = JSONSchema4[] | JSONSchema6[] | JSONSchema7[] +type JSONSchemaArrType = JSONSchemaObjectType[] type Properties = Record; type ArrayItems = JSONSchemaObjectType | JSONSchemaArrType | boolean; @@ -33,16 +33,17 @@ const buildSchemaFilter = ( if (typeof items === 'object') { console.log("ARRAY FOUND PROPS") return pipe( - O.fromNullable((items as JSONSchemaObjectType).properties), - O.chainNullableK(properties => properties as Properties) + O.fromNullable((items as JSONSchemaObjectType).properties as Properties), + // O.chainNullableK(properties => properties as Properties) ) } return O.none }), O.alt(() => { console.log("NORMAL NON ARRAY PROPS") - return O.fromNullable(schema.properties)}), - O.chain(unfilteredProps => filterPropertiesHelper(O.fromNullable(unfilteredProps as Properties))), + return O.fromNullable(schema.properties) + }), + O.chain((unfilteredProps: Properties) => filterPropertiesHelper(O.fromNullable(unfilteredProps))), O.map(filteredProperties => { console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema) if (schema.items && typeof schema.items === 'object') { @@ -70,45 +71,38 @@ const buildSchemaFilter = ( function filterTupleTypedProperties(schema: S): Option { console.log("SCHEMA ITEMS", schema.items) return pipe( - O.fromNullable(schema.items as Properties[]), + O.fromNullable(schema.items as JSONSchemaArrType), O.chain(items => { return pipe( - items, - A.map(item => (item as JSONSchemaObjectType).properties), - propertiesArray => O.fromNullable(propertiesArray as Properties[]) // Casting because Properties is likely expected to be an object, not an array - ) - } - ), + items, + A.map(item => (item as JSONSchemaObjectType).properties), + propertiesArray => O.fromNullable(propertiesArray as Properties[]) // Casting because Properties is likely expected to be an object, not an array + ) + }), O.map(unfilteredProps => { return pipe( - unfilteredProps, - A.map(unfilteredProp => filterPropertiesHelper(O.fromNullable(unfilteredProp as Properties))), + unfilteredProps, + A.map(unfilteredProp => filterPropertiesHelper(O.fromNullable(unfilteredProp as Properties))), + ) + }), + O.map(filteredProperties => { + const items = pipe( + A.zip(schema.items as JSONSchemaArrType, filteredProperties), + A.map(([item, properties]) => ({ + ...item, + properties: pipe( + properties, + O.getOrElse(() => ({} as object)) ) - } - ), - O.chain(filteredProperties => { - console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema) - if (schema.items && typeof schema.items === 'object') { - const items = (schema.items as JSONSchemaArrType).map((item, index) => { - const defaultProperty = {}; - const properties = - pipe( - filteredProperties[index], - O.getOrElse(() => defaultProperty) - ); - return { ...item, properties }; - }); - - return O.some({ - ...schema, - items: { - ...items - }, - }); - } - return O.none - } - ), + })) + ); + return { + ...schema, + items: [ + ...items + ], + }; + }), O.alt(() => { console.log("RESULT SCHEMA", schema) return O.some(schema) @@ -116,46 +110,6 @@ const buildSchemaFilter = ( ); } - function filterNonTupleTypedRequired(updatedSchema: S, originalSchema: S): Option { - function getCorrectSchema(schema: S) { - if (schema.items && typeof schema.items === 'object') { // i.e. this is an array - return (schema.items as JSONSchemaObjectType); - } - return schema; // i.e. this is an object - } - - return pipe( - updatedSchema, - O.fromPredicate(schema => Array.isArray(getCorrectSchema(schema).required)), - O.map(schema => Object.keys(getCorrectSchema(schema).properties || {})), - O.map(updatedProperties => { - const originalPropertyNames = Object.keys(getCorrectSchema(originalSchema).properties || {}); - return originalPropertyNames.filter(name => !updatedProperties.includes(name)); - }), - O.map(removedProperties => { - const required = getCorrectSchema(originalSchema).required - return (required as string[]).filter(name => !removedProperties.includes(name)) - }), - O.map(required => { - console.log("UPDATED SCHEMA", updatedSchema, "REQUIRED", required) - if (updatedSchema.items && typeof updatedSchema.items === 'object') { - return { - ...updatedSchema, - items: { - ...updatedSchema.items, - required: required, - }, - }; - } - return { - ...updatedSchema, - required: required, - } - }), - O.alt(() => O.some(updatedSchema)) - ); - } - function filterPropertiesHelper(properties: Option): Option { return pipe( properties, @@ -191,6 +145,97 @@ const buildSchemaFilter = ( ) } + function filterRequiredHelper(updatedSchema: S | JSONSchemaObjectType, originalSchema: S | JSONSchemaObjectType): Option { + return pipe ( + updatedSchema, + O.fromPredicate(schema => Array.isArray(schema.required)), + O.map(schema => Object.keys(schema.properties || {})), + O.map(updatedProperties => { + const originalPropertyNames = Object.keys(originalSchema.properties || {}); + return originalPropertyNames.filter(name => !updatedProperties.includes(name)); + }), + O.map(removedProperties => { + const required = originalSchema.required + return (required as string[]).filter(name => !removedProperties.includes(name)) + }), + ) + } + + function filterNonTupleTypedRequired(updatedSchema: S, originalSchema: S): Option { + function getCorrectSchema(schema: S) { + if (schema.items && typeof schema.items === 'object') { // i.e. this is an array + return (schema.items as JSONSchemaObjectType); + } + return schema; // i.e. this is an object + } + + return pipe( + updatedSchema, + schema => filterRequiredHelper(getCorrectSchema(schema), getCorrectSchema(originalSchema)), + O.map(required => { + console.log("UPDATED SCHEMA", updatedSchema, "REQUIRED", required) + if (updatedSchema.items && typeof updatedSchema.items === 'object') { + return { + ...updatedSchema, + items: { + ...updatedSchema.items, + required: required, + }, + }; + } + return { + ...updatedSchema, + required: required, + } + }), + O.alt(() => O.some(updatedSchema)) + ); + } + + function filterTupleTypedRequired(updatedSchema: S, originalSchema: S): Option { + return pipe( + O.fromNullable(updatedSchema.items as JSONSchemaArrType), + O.chain(itemSchemas => { + return pipe( + O.fromNullable(originalSchema.items as JSONSchemaArrType), + O.map(originalItemSchemas => { + console.log("ITEM SCHEMAS", itemSchemas, "ORIGINAL ITEM SCHEMAS", originalSchema.items) + return A.zip(itemSchemas, originalItemSchemas)}), + O.map(zippedSchemas => { + console.log("HERE", zippedSchemas) + return zippedSchemas.map(([itemSchema, originalItemSchema]) => filterRequiredHelper(itemSchema, originalItemSchema)) + } + ) + ) + }), + O.map(requiredList => { + console.log("REQUIRED LIST", requiredList) + const items = pipe( + A.zip(updatedSchema.items as JSONSchemaArrType, requiredList), + A.map(([item, required]) => { + console.log("ITEM", item, "REQUIRED", required) + return { + ...item, + required: pipe( + required, + O.getOrElse(() => [] as string[]) + ) + } + } + ) + ); + + return { + ...updatedSchema, + items: { + ...items + }, + } + }), + O.alt(() => O.some(updatedSchema)) + ); + } + function filter(inputSchema: S): Option { return pipe( inputSchema, @@ -202,7 +247,12 @@ const buildSchemaFilter = ( } return filterNonTupleTypedProperties(inputSchema) } ), - O.chain(schema => filterNonTupleTypedRequired(schema, inputSchema)) + O.chain(schema => { + if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing + return filterTupleTypedRequired(schema, inputSchema) + } + return filterNonTupleTypedRequired(schema, inputSchema) + }) ); } From 0a869cdd13a6906a623a8fcfb3dd1bbb1f04a18a Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Wed, 17 Apr 2024 21:25:48 -0700 Subject: [PATCH 08/11] (very messy) version with additionalItems working for properties only, not required --- .../filterRequiredProperties.test.ts | 49 ++++---- .../src/utils/filterRequiredProperties.ts | 106 ++++++++++++------ 2 files changed, 100 insertions(+), 55 deletions(-) diff --git a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts index a5f222b1c..b26c58849 100644 --- a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts +++ b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts @@ -148,36 +148,47 @@ describe('filterRequiredProperties', () => { }, { type: 'object', - required: ['id', 'name'], - properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + required: ['address', 'title'], + properties: { address: { readOnly: true, type: 'string' }, title: { type: 'string' } } } ], additionalItems: { type: 'object', properties: { - status: { type: 'string' } + status: { readOnly: true, type: 'string' }, + ticket: { type: 'string' } }, - required: ['status'] + required: ['status', 'ticket'] } } } }; - // assertSome(stripReadOnlyProperties(schema), schema => { - // console.log("RESULT", schema.properties) - // expect(schema.properties).not.toBeNull() - // if (schema.properties) { - // const arr_items = (schema.properties.objectsArrayWithAdditionalItems as JSONSchema).items as JSONSchema - // expect(arr_items).not.toBeNull() - // if (arr_items){ - // console.log("RESULT2", arr_items) - // expect(arr_items.required).toEqual(['name']); - // expect(arr_items.properties).toEqual({ - // name: expect.any(Object), - // }); - // } - // } - // }); + assertSome(stripReadOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + console.log("FINAL", schema.properties) + const arr = schema.properties.objectsArrayWithAdditionalItems as JSONSchema + const arr_items = arr.items as JSONSchema + console.log("FINAL ARR", arr_items) + + expect(arr_items).not.toBeNull() + expect(arr_items[0].required).toEqual(['name']); + expect(arr_items[0].properties).toEqual({ + name: expect.any(Object) + }); + expect(arr_items[1].required).toEqual(['title']); + expect(arr_items[1].properties).toEqual({ + title: expect.any(Object) + }); + + expect(arr.additionalItems).not.toBeNull() + const additional_items = arr.additionalItems as JSONSchema + expect(additional_items.properties).toEqual({ + ticket: expect.any(Object) + }) + } + }); }); it('strips readOnly properties from objects within tuple-typed array no additionalItems', () => { diff --git a/packages/http/src/utils/filterRequiredProperties.ts b/packages/http/src/utils/filterRequiredProperties.ts index 32ae06dc9..6cadff199 100644 --- a/packages/http/src/utils/filterRequiredProperties.ts +++ b/packages/http/src/utils/filterRequiredProperties.ts @@ -25,37 +25,51 @@ type RequiredSchemaSubset = { const buildSchemaFilter = ( keepPropertyPredicate: (schema: S) => Option ): ((schema: S) => Option) => { - function filterNonTupleTypedProperties(schema: S): Option { - console.log("SCHEMA ITEMS", schema.items) + function filterPropertiesFromObjectSingle(schema: S): Option { + console.log("SCHEMA ITEMS", schema.items, "SCHEMA", schema) return pipe( O.fromNullable(schema.items), O.chain(items => { - if (typeof items === 'object') { - console.log("ARRAY FOUND PROPS") - return pipe( - O.fromNullable((items as JSONSchemaObjectType).properties as Properties), - // O.chainNullableK(properties => properties as Properties) - ) - } - return O.none + // the schema is an array with a single-schema item, i.e. non-tuple typing + return O.fromNullable((items as JSONSchemaObjectType).properties as Properties) }), - O.alt(() => { - console.log("NORMAL NON ARRAY PROPS") - return O.fromNullable(schema.properties) + O.alt(() => O.fromNullable(schema.properties)), // the schema is an object that's not an array + O.alt(() => { + // the schema is an tuple-typed array with additionalItems defined + return pipe ( + O.fromNullable(schema.additionalItems as JSONSchemaObjectType), + O.map(additionalItems => additionalItems.properties as Properties) + ) }), O.chain((unfilteredProps: Properties) => filterPropertiesHelper(O.fromNullable(unfilteredProps))), O.map(filteredProperties => { - console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema) - if (schema.items && typeof schema.items === 'object') { - return { - ...schema, - items: { - ...schema.items, - properties: filteredProperties, - }, - }; - } - return { + console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema, "ITEMS", schema.items) + if (schema.items) { // the schema is an array + console.log("I HAVE FOUND SCHEMA ITEMS", schema.items, typeof schema.items === 'object', Array.isArray(schema.items), "ADDITIONAL", schema.additionalItems, typeof schema.additionalItems === 'object') + // console.log("I AM HERE") + if (Array.isArray(schema.items) && typeof schema.additionalItems === 'object') { // tuple typed array with additionalItems specified + console.log("FOUND ADDITIONAL ITEMS, RESULTING SCHEMA", schema, "ADDITIONAL ITEMS ORIGINAL", schema.additionalItems) + return { + ...schema, + additionalItems: { + ...schema.additionalItems, + properties: filteredProperties + } + }; + } else if (typeof schema.items === 'object') { // the array is non-tuple typed + console.log("ARRAY IS OBJECT") + return { + ...schema, + items: { + ...schema.items, + properties: filteredProperties, + }, + }; + } + + + } + return { // the schema is an object ...schema, properties: filteredProperties, } @@ -68,7 +82,7 @@ const buildSchemaFilter = ( ); } - function filterTupleTypedProperties(schema: S): Option { + function filterPropertiesFromObjectsList(schema: S): Option { console.log("SCHEMA ITEMS", schema.items) return pipe( O.fromNullable(schema.items as JSONSchemaArrType), @@ -161,7 +175,7 @@ const buildSchemaFilter = ( ) } - function filterNonTupleTypedRequired(updatedSchema: S, originalSchema: S): Option { + function filterRequiredFromObjectSingle(updatedSchema: S, originalSchema: S): Option { function getCorrectSchema(schema: S) { if (schema.items && typeof schema.items === 'object') { // i.e. this is an array return (schema.items as JSONSchemaObjectType); @@ -192,7 +206,7 @@ const buildSchemaFilter = ( ); } - function filterTupleTypedRequired(updatedSchema: S, originalSchema: S): Option { + function filterRequiredFromObjectsList(updatedSchema: S, originalSchema: S): Option { return pipe( O.fromNullable(updatedSchema.items as JSONSchemaArrType), O.chain(itemSchemas => { @@ -227,9 +241,9 @@ const buildSchemaFilter = ( return { ...updatedSchema, - items: { + items: [ ...items - }, + ], } }), O.alt(() => O.some(updatedSchema)) @@ -241,17 +255,37 @@ const buildSchemaFilter = ( inputSchema, keepPropertyPredicate, O.chain(inputSchema => { - console.log("INPUT SCHEMA", inputSchema) - if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing - return filterTupleTypedProperties(inputSchema) - } - return filterNonTupleTypedProperties(inputSchema) + + return pipe( + O.fromNullable(inputSchema), + O.chain(schema => + Array.isArray(schema.items) ? + pipe( + filterPropertiesFromObjectsList(schema), + O.alt(() => O.some(schema)), // Use the original schema if filterPropertiesFromObjectsList returns None + O.chain(filteredSchema => { + console.log("HEREEEE", filteredSchema, filteredSchema.items) + return filterPropertiesFromObjectSingle(filteredSchema) + }) + ) : + filterPropertiesFromObjectSingle(schema) + ) + ); + + // if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing + // return pipe( + // O.fromNullable(inputSchema), + // filterPropertiesFromObjectsList(inputSchema), + // filterPropertiesFromObjectSingle(inputSchema) + // ) + // } + // return filterPropertiesFromObjectSingle(inputSchema) } ), O.chain(schema => { if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing - return filterTupleTypedRequired(schema, inputSchema) + return filterRequiredFromObjectsList(schema, inputSchema) } - return filterNonTupleTypedRequired(schema, inputSchema) + return filterRequiredFromObjectSingle(schema, inputSchema) }) ); } From d413946304eb39236ffdf122ed0a21f54e5bd74b Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Thu, 18 Apr 2024 01:38:21 -0700 Subject: [PATCH 09/11] (very very messy) all tests pass for complete cases of arrays, tuple typed including additionalItems, as well as single typed --- .../filterRequiredProperties.test.ts | 41 ++++--- .../src/utils/filterRequiredProperties.ts | 101 +++++++++++------- 2 files changed, 89 insertions(+), 53 deletions(-) diff --git a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts index b26c58849..46e20cceb 100644 --- a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts +++ b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts @@ -187,11 +187,12 @@ describe('filterRequiredProperties', () => { expect(additional_items.properties).toEqual({ ticket: expect.any(Object) }) + expect(additional_items.required).toEqual(['ticket']); } }); }); - it('strips readOnly properties from objects within tuple-typed array no additionalItems', () => { + it('strips readOnly properties from objects in tuple-typed array no additionalItems', () => { const schema: JSONSchema = { type: 'object', properties: { @@ -205,8 +206,8 @@ describe('filterRequiredProperties', () => { }, { type: 'object', - required: ['id', 'name'], - properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + required: ['address', 'title'], + properties: { address: { readOnly: true, type: 'string' }, title: { type: 'string' } } } ], additionalItems: false @@ -214,19 +215,27 @@ describe('filterRequiredProperties', () => { } }; - // assertSome(stripReadOnlyProperties(schema), schema => { - // expect(schema.properties).not.toBeNull() - // if (schema.properties) { - // const arr_items = (schema.properties.objectsArrayNoAdditionalItems as JSONSchema).items as JSONSchema - // expect(arr_items).not.toBeNull() - // if (arr_items){ - // expect(arr_items.required).toEqual(['name']); - // expect(arr_items.properties).toEqual({ - // name: expect.any(Object), - // }); - // } - // } - // }); + assertSome(stripReadOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + console.log("FINAL", schema.properties) + const arr = schema.properties.objectsArrayNoAdditionalItems as JSONSchema + const arr_items = arr.items as JSONSchema + console.log("FINAL ARR", arr_items) + + expect(arr_items).not.toBeNull() + expect(arr_items[0].required).toEqual(['name']); + expect(arr_items[0].properties).toEqual({ + name: expect.any(Object) + }); + expect(arr_items[1].required).toEqual(['title']); + expect(arr_items[1].properties).toEqual({ + title: expect.any(Object) + }); + + expect(arr.additionalItems).toEqual(false) + } + }); }); it('strips nested writeOnly properties', () => { diff --git a/packages/http/src/utils/filterRequiredProperties.ts b/packages/http/src/utils/filterRequiredProperties.ts index 6cadff199..a65ac0d5f 100644 --- a/packages/http/src/utils/filterRequiredProperties.ts +++ b/packages/http/src/utils/filterRequiredProperties.ts @@ -26,7 +26,7 @@ const buildSchemaFilter = ( keepPropertyPredicate: (schema: S) => Option ): ((schema: S) => Option) => { function filterPropertiesFromObjectSingle(schema: S): Option { - console.log("SCHEMA ITEMS", schema.items, "SCHEMA", schema) + // console.log("SCHEMA ITEMS", schema.items, "SCHEMA", schema) return pipe( O.fromNullable(schema.items), O.chain(items => { @@ -43,12 +43,12 @@ const buildSchemaFilter = ( }), O.chain((unfilteredProps: Properties) => filterPropertiesHelper(O.fromNullable(unfilteredProps))), O.map(filteredProperties => { - console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema, "ITEMS", schema.items) + // console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema, "ITEMS", schema.items) if (schema.items) { // the schema is an array - console.log("I HAVE FOUND SCHEMA ITEMS", schema.items, typeof schema.items === 'object', Array.isArray(schema.items), "ADDITIONAL", schema.additionalItems, typeof schema.additionalItems === 'object') + // console.log("I HAVE FOUND SCHEMA ITEMS", schema.items, typeof schema.items === 'object', Array.isArray(schema.items), "ADDITIONAL", schema.additionalItems, typeof schema.additionalItems === 'object') // console.log("I AM HERE") if (Array.isArray(schema.items) && typeof schema.additionalItems === 'object') { // tuple typed array with additionalItems specified - console.log("FOUND ADDITIONAL ITEMS, RESULTING SCHEMA", schema, "ADDITIONAL ITEMS ORIGINAL", schema.additionalItems) + // console.log("FOUND ADDITIONAL ITEMS, RESULTING SCHEMA", schema, "ADDITIONAL ITEMS ORIGINAL", schema.additionalItems) return { ...schema, additionalItems: { @@ -57,7 +57,7 @@ const buildSchemaFilter = ( } }; } else if (typeof schema.items === 'object') { // the array is non-tuple typed - console.log("ARRAY IS OBJECT") + // console.log("ARRAY IS OBJECT") return { ...schema, items: { @@ -76,14 +76,14 @@ const buildSchemaFilter = ( } ), O.alt(() => { - console.log("RESULT SCHEMA", schema) + // console.log("RESULT SCHEMA", schema) return O.some(schema) }) ); } function filterPropertiesFromObjectsList(schema: S): Option { - console.log("SCHEMA ITEMS", schema.items) + // console.log("SCHEMA ITEMS", schema.items) return pipe( O.fromNullable(schema.items as JSONSchemaArrType), O.chain(items => { @@ -118,7 +118,7 @@ const buildSchemaFilter = ( }; }), O.alt(() => { - console.log("RESULT SCHEMA", schema) + // console.log("RESULT SCHEMA", schema) return O.some(schema) }) ); @@ -136,13 +136,13 @@ const buildSchemaFilter = ( return pipe( properties[propertyName], O.fromPredicate(p => { - console.log("properties", properties) - console.log("propertyName", propertyName) + // console.log("properties", properties) + // console.log("propertyName", propertyName) if (typeof p === 'boolean') { // I think this is bc Object.keys only handles string props so boolean props need to be added back in manually filteredProperties[propertyName] = properties[propertyName]; return false; } - console.log("P", p) + // console.log("P", p) return true; }), O.chain(p => filter(p as S)), @@ -160,7 +160,7 @@ const buildSchemaFilter = ( } function filterRequiredHelper(updatedSchema: S | JSONSchemaObjectType, originalSchema: S | JSONSchemaObjectType): Option { - return pipe ( + const x = pipe ( updatedSchema, O.fromPredicate(schema => Array.isArray(schema.required)), O.map(schema => Object.keys(schema.properties || {})), @@ -170,25 +170,46 @@ const buildSchemaFilter = ( }), O.map(removedProperties => { const required = originalSchema.required + // console.log("HEHEHRERER", required, "REMOVED PROPS", removedProperties) + // console.log("RES", (required as string[]).filter(name => !removedProperties.includes(name))) return (required as string[]).filter(name => !removedProperties.includes(name)) }), ) + console.log("INTERMEDIATE", x) + return x } function filterRequiredFromObjectSingle(updatedSchema: S, originalSchema: S): Option { function getCorrectSchema(schema: S) { - if (schema.items && typeof schema.items === 'object') { // i.e. this is an array - return (schema.items as JSONSchemaObjectType); + if (Array.isArray(schema.items) && typeof schema.additionalItems === 'object') { + return schema.additionalItems; // we're looking at the additionItems object in a schema with a tuple-typed array + } else if (typeof schema.items === 'object') { + return (schema.items as JSONSchemaObjectType); // we're looking at the item schema for a non-tuple-typed array } - return schema; // i.e. this is an object + return schema; // schema is not an array } return pipe( updatedSchema, - schema => filterRequiredHelper(getCorrectSchema(schema), getCorrectSchema(originalSchema)), + schema => { + console.log("HERE1") + const x = filterRequiredHelper(getCorrectSchema(schema), getCorrectSchema(originalSchema)) + console.log("HERE2", x) + return x + }, O.map(required => { - console.log("UPDATED SCHEMA", updatedSchema, "REQUIRED", required) - if (updatedSchema.items && typeof updatedSchema.items === 'object') { + // console.log("UPDATED SCHEMA", updatedSchema, "REQUIRED", required) + // console.log("Array.isArray(updatedSchema.items)", updatedSchema.items, Array.isArray(updatedSchema.items), updatedSchema.additionalItems, typeof updatedSchema.additionalItems === 'object') + if (Array.isArray(updatedSchema.items) && typeof updatedSchema.additionalItems === 'object') { + return { + ...updatedSchema, + additionalItems: { + ...updatedSchema.additionalItems, + required: required + } + }; + } + else if (updatedSchema.items && typeof updatedSchema.items === 'object') { return { ...updatedSchema, items: { @@ -213,21 +234,21 @@ const buildSchemaFilter = ( return pipe( O.fromNullable(originalSchema.items as JSONSchemaArrType), O.map(originalItemSchemas => { - console.log("ITEM SCHEMAS", itemSchemas, "ORIGINAL ITEM SCHEMAS", originalSchema.items) + // console.log("ITEM SCHEMAS", itemSchemas, "ORIGINAL ITEM SCHEMAS", originalSchema.items) return A.zip(itemSchemas, originalItemSchemas)}), O.map(zippedSchemas => { - console.log("HERE", zippedSchemas) + // console.log("HERE", zippedSchemas) return zippedSchemas.map(([itemSchema, originalItemSchema]) => filterRequiredHelper(itemSchema, originalItemSchema)) } ) ) }), O.map(requiredList => { - console.log("REQUIRED LIST", requiredList) + // console.log("REQUIRED LIST", requiredList) const items = pipe( A.zip(updatedSchema.items as JSONSchemaArrType, requiredList), A.map(([item, required]) => { - console.log("ITEM", item, "REQUIRED", required) + // console.log("ITEM", item, "REQUIRED", required) return { ...item, required: pipe( @@ -262,30 +283,36 @@ const buildSchemaFilter = ( Array.isArray(schema.items) ? pipe( filterPropertiesFromObjectsList(schema), - O.alt(() => O.some(schema)), // Use the original schema if filterPropertiesFromObjectsList returns None + O.alt(() => O.some(schema)), O.chain(filteredSchema => { - console.log("HEREEEE", filteredSchema, filteredSchema.items) + // console.log("HEREEEE", filteredSchema, filteredSchema.items) return filterPropertiesFromObjectSingle(filteredSchema) }) ) : filterPropertiesFromObjectSingle(schema) ) ); - - // if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing - // return pipe( - // O.fromNullable(inputSchema), - // filterPropertiesFromObjectsList(inputSchema), - // filterPropertiesFromObjectSingle(inputSchema) - // ) - // } - // return filterPropertiesFromObjectSingle(inputSchema) } ), O.chain(schema => { - if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing - return filterRequiredFromObjectsList(schema, inputSchema) - } - return filterRequiredFromObjectSingle(schema, inputSchema) + return pipe( + O.fromNullable(schema), + O.chain(schema => + Array.isArray(schema.items) ? + pipe( + filterRequiredFromObjectsList(schema, inputSchema), + O.alt(() => O.some(schema)), + O.chain(filteredSchema => { + // console.log("HEREEEE", filteredSchema, filteredSchema.items) + return filterRequiredFromObjectSingle(filteredSchema, inputSchema) + }) + ) : + filterRequiredFromObjectSingle(schema, inputSchema) + ) + ); + // if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing + // return filterRequiredFromObjectsList(schema, inputSchema) + // } + // return filterRequiredFromObjectSingle(schema, inputSchema) }) ); } From 5a9e794fdb1169ee2c1a5a4d141ab5dcfda7dd73 Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Thu, 18 Apr 2024 11:18:50 -0700 Subject: [PATCH 10/11] clean up code --- .../filterRequiredProperties.test.ts | 50 +-- .../src/utils/filterRequiredProperties.ts | 316 +++++++----------- 2 files changed, 152 insertions(+), 214 deletions(-) diff --git a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts index 46e20cceb..41d018ce9 100644 --- a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts +++ b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts @@ -23,7 +23,7 @@ describe('filterRequiredProperties', () => { }); }); - it('strips readOnly properties on standalone object', () => { + it('strips readOnly properties on non-array object', () => { const schema: JSONSchema = { type: 'object', properties: { @@ -72,11 +72,12 @@ describe('filterRequiredProperties', () => { assertSome(stripReadOnlyProperties(schema), schema => { expect(schema.properties).not.toBeNull() if (schema.properties) { - console.log("FINAL", schema.properties) - const arr_items = (schema.properties.objectsArray as JSONSchema).items as JSONSchema - console.log("FINAL ARR", arr_items) - expect(arr_items).not.toBeNull() - if (arr_items){ + const arr_items = (schema.properties.objectsArray as JSONSchema).items as JSONSchema; + + expect(arr_items).not.toBeNull(); + expect(arr_items).not.toBeUndefined(); + + if (arr_items) { expect(arr_items.required).toEqual(['name']); expect(arr_items.properties).toEqual({ name: expect.any(Object), @@ -116,11 +117,12 @@ describe('filterRequiredProperties', () => { assertSome(stripReadOnlyProperties(schema), schema => { expect(schema.properties).not.toBeNull() if (schema.properties) { - console.log("FINAL", schema.properties) - const arr_items = (schema.properties.objectsArrayAdditionalItemsUnspecified as JSONSchema).items as JSONSchema - console.log("FINAL ARR", arr_items) - expect(arr_items).not.toBeNull() - if (arr_items){ + const arr_items = (schema.properties.objectsArrayAdditionalItemsUnspecified as JSONSchema).items; + + expect(arr_items).not.toBeNull(); + expect(arr_items).not.toBeUndefined(); + + if (arr_items) { expect(arr_items[0].required).toEqual(['name']); expect(arr_items[0].properties).toEqual({ name: expect.any(Object), @@ -167,12 +169,12 @@ describe('filterRequiredProperties', () => { assertSome(stripReadOnlyProperties(schema), schema => { expect(schema.properties).not.toBeNull() if (schema.properties) { - console.log("FINAL", schema.properties) - const arr = schema.properties.objectsArrayWithAdditionalItems as JSONSchema - const arr_items = arr.items as JSONSchema - console.log("FINAL ARR", arr_items) + const arr = schema.properties.objectsArrayWithAdditionalItems as JSONSchema; - expect(arr_items).not.toBeNull() + expect(arr.items).not.toBeNull(); + expect(arr.items).not.toBeUndefined(); + + const arr_items = arr.items as JSONSchema; expect(arr_items[0].required).toEqual(['name']); expect(arr_items[0].properties).toEqual({ name: expect.any(Object) @@ -182,11 +184,11 @@ describe('filterRequiredProperties', () => { title: expect.any(Object) }); - expect(arr.additionalItems).not.toBeNull() + expect(arr.additionalItems).not.toBeNull(); const additional_items = arr.additionalItems as JSONSchema expect(additional_items.properties).toEqual({ ticket: expect.any(Object) - }) + }); expect(additional_items.required).toEqual(['ticket']); } }); @@ -218,12 +220,12 @@ describe('filterRequiredProperties', () => { assertSome(stripReadOnlyProperties(schema), schema => { expect(schema.properties).not.toBeNull() if (schema.properties) { - console.log("FINAL", schema.properties) - const arr = schema.properties.objectsArrayNoAdditionalItems as JSONSchema - const arr_items = arr.items as JSONSchema - console.log("FINAL ARR", arr_items) + const arr = schema.properties.objectsArrayNoAdditionalItems as JSONSchema; - expect(arr_items).not.toBeNull() + expect(arr.items).not.toBeNull(); + expect(arr.items).not.toBeUndefined(); + + const arr_items = arr.items as JSONSchema; expect(arr_items[0].required).toEqual(['name']); expect(arr_items[0].properties).toEqual({ name: expect.any(Object) @@ -233,7 +235,7 @@ describe('filterRequiredProperties', () => { title: expect.any(Object) }); - expect(arr.additionalItems).toEqual(false) + expect(arr.additionalItems).toEqual(false); } }); }); diff --git a/packages/http/src/utils/filterRequiredProperties.ts b/packages/http/src/utils/filterRequiredProperties.ts index a65ac0d5f..d2671ec6b 100644 --- a/packages/http/src/utils/filterRequiredProperties.ts +++ b/packages/http/src/utils/filterRequiredProperties.ts @@ -4,104 +4,83 @@ import * as O from 'fp-ts/Option'; import { Option } from 'fp-ts/Option'; import * as A from 'fp-ts/Array'; import { JSONSchema } from '../types'; -import { update } from 'lodash'; type JSONSchemaObjectType = JSONSchema6 | JSONSchema7 | JSONSchema4 -type JSONSchemaArrType = JSONSchemaObjectType[] - type Properties = Record; -type ArrayItems = JSONSchemaObjectType | JSONSchemaArrType | boolean; -type ArrayAdditionalItems = JSONSchemaObjectType | boolean; +/** + * for items type signature: see https://tools.ietf.org/html/draft-zyp-json-schema-03, + * sections 5.5, 6.4, and 6.9 + * for additionalItems type signature, see sections 5.6, 6.4, and 6.10 + */ type RequiredSchemaSubset = { readOnly?: boolean; writeOnly?: boolean; properties?: Properties; required?: string[] | false; - items?: ArrayItems; - additionalItems?: ArrayAdditionalItems; + items?: JSONSchemaObjectType | JSONSchemaObjectType[] | boolean; + additionalItems?: JSONSchemaObjectType | boolean; }; const buildSchemaFilter = ( keepPropertyPredicate: (schema: S) => Option ): ((schema: S) => Option) => { + // Helper function to filter properties from a *single* object (as opposed to a list of objects) function filterPropertiesFromObjectSingle(schema: S): Option { - // console.log("SCHEMA ITEMS", schema.items, "SCHEMA", schema) return pipe( - O.fromNullable(schema.items), - O.chain(items => { - // the schema is an array with a single-schema item, i.e. non-tuple typing - return O.fromNullable((items as JSONSchemaObjectType).properties as Properties) - }), - O.alt(() => O.fromNullable(schema.properties)), // the schema is an object that's not an array - O.alt(() => { - // the schema is an tuple-typed array with additionalItems defined - return pipe ( - O.fromNullable(schema.additionalItems as JSONSchemaObjectType), - O.map(additionalItems => additionalItems.properties as Properties) - ) - }), + O.fromNullable(schema.items as S), + O.chain(items => O.fromNullable((items as S).properties as Properties)), // the schema is an array with a single-typed item, i.e. non-tuple typing + O.alt(() => O.fromNullable(schema.properties)), // the schema is an object that's not an array + O.alt(() => pipe ( // the schema is an tuple-typed array with additionalItems schema defined + O.fromNullable(schema.additionalItems as S), + O.map(additionalItems => additionalItems.properties as Properties) + )), O.chain((unfilteredProps: Properties) => filterPropertiesHelper(O.fromNullable(unfilteredProps))), O.map(filteredProperties => { - // console.log("FILTERED PROPS", filteredProperties, "SCHEMA", schema, "ITEMS", schema.items) if (schema.items) { // the schema is an array - // console.log("I HAVE FOUND SCHEMA ITEMS", schema.items, typeof schema.items === 'object', Array.isArray(schema.items), "ADDITIONAL", schema.additionalItems, typeof schema.additionalItems === 'object') - // console.log("I AM HERE") - if (Array.isArray(schema.items) && typeof schema.additionalItems === 'object') { // tuple typed array with additionalItems specified - // console.log("FOUND ADDITIONAL ITEMS, RESULTING SCHEMA", schema, "ADDITIONAL ITEMS ORIGINAL", schema.additionalItems) - return { + if (Array.isArray(schema.items) && typeof schema.additionalItems === 'object') { + return { // the array is tuple-typed with additionalItems schema object specified ...schema, additionalItems: { ...schema.additionalItems, properties: filteredProperties } }; - } else if (typeof schema.items === 'object') { // the array is non-tuple typed - // console.log("ARRAY IS OBJECT") - return { - ...schema, - items: { - ...schema.items, - properties: filteredProperties, - }, - }; - } - - - } - return { // the schema is an object + } else if (typeof schema.items === 'object') { + return { // the array is non-tuple typed + ...schema, + items: { + ...schema.items, + properties: filteredProperties, + }, + }; + } + } + return { // the schema is a non-array object ...schema, properties: filteredProperties, - } - } - ), - O.alt(() => { - // console.log("RESULT SCHEMA", schema) - return O.some(schema) - }) + }; + }), + O.alt(() => O.some(schema)) ); } + // Helper function to filter properties from a list of objects function filterPropertiesFromObjectsList(schema: S): Option { - // console.log("SCHEMA ITEMS", schema.items) return pipe( - O.fromNullable(schema.items as JSONSchemaArrType), - O.chain(items => { - return pipe( - items, - A.map(item => (item as JSONSchemaObjectType).properties), - propertiesArray => O.fromNullable(propertiesArray as Properties[]) // Casting because Properties is likely expected to be an object, not an array - ) - }), - O.map(unfilteredProps => { - return pipe( - unfilteredProps, - A.map(unfilteredProp => filterPropertiesHelper(O.fromNullable(unfilteredProp as Properties))), - ) - }), + O.fromNullable(schema.items as S[]), + O.chain(items => pipe( + items, + A.map(item => (item as S).properties), + propertiesArray => O.fromNullable(propertiesArray) + )), + O.map(unfilteredProps => pipe( + unfilteredProps, + A.map(unfilteredProp => filterPropertiesHelper(O.fromNullable(unfilteredProp))) + )), O.map(filteredProperties => { const items = pipe( - A.zip(schema.items as JSONSchemaArrType, filteredProperties), + A.zip(schema.items as S[], filteredProperties), A.map(([item, properties]) => ({ ...item, properties: pipe( @@ -114,13 +93,10 @@ const buildSchemaFilter = ( ...schema, items: [ ...items - ], + ] }; }), - O.alt(() => { - // console.log("RESULT SCHEMA", schema) - return O.some(schema) - }) + O.alt(() => O.some(schema)) ); } @@ -132,74 +108,43 @@ const buildSchemaFilter = ( Object.keys(properties), A.reduce( {} as Properties, - (filteredProperties: Properties, propertyName): Properties => { - return pipe( - properties[propertyName], - O.fromPredicate(p => { - // console.log("properties", properties) - // console.log("propertyName", propertyName) - if (typeof p === 'boolean') { // I think this is bc Object.keys only handles string props so boolean props need to be added back in manually - filteredProperties[propertyName] = properties[propertyName]; - return false; - } - // console.log("P", p) - return true; - }), - O.chain(p => filter(p as S)), - O.map(v => ({ ...filteredProperties, [propertyName]: v } as Properties)), - O.fold( - () => filteredProperties, - v => v - ) - ); - } + (filteredProperties: Properties, propertyName): Properties => pipe( + properties[propertyName], + O.fromPredicate(p => { + if (typeof p === 'boolean') { + filteredProperties[propertyName] = properties[propertyName]; + return false; + } + return true; + }), + O.chain(p => filter(p as S)), + O.map(v => ({ ...filteredProperties, [propertyName]: v } as Properties)), + O.fold( + () => filteredProperties, + v => v + ) + ) ) ) ) ) } - function filterRequiredHelper(updatedSchema: S | JSONSchemaObjectType, originalSchema: S | JSONSchemaObjectType): Option { - const x = pipe ( - updatedSchema, - O.fromPredicate(schema => Array.isArray(schema.required)), - O.map(schema => Object.keys(schema.properties || {})), - O.map(updatedProperties => { - const originalPropertyNames = Object.keys(originalSchema.properties || {}); - return originalPropertyNames.filter(name => !updatedProperties.includes(name)); - }), - O.map(removedProperties => { - const required = originalSchema.required - // console.log("HEHEHRERER", required, "REMOVED PROPS", removedProperties) - // console.log("RES", (required as string[]).filter(name => !removedProperties.includes(name))) - return (required as string[]).filter(name => !removedProperties.includes(name)) - }), - ) - console.log("INTERMEDIATE", x) - return x - } - + // Helper function to filter required properties from a *single* object (as opposed to a list of objects) function filterRequiredFromObjectSingle(updatedSchema: S, originalSchema: S): Option { function getCorrectSchema(schema: S) { if (Array.isArray(schema.items) && typeof schema.additionalItems === 'object') { - return schema.additionalItems; // we're looking at the additionItems object in a schema with a tuple-typed array + return schema.additionalItems as S; // we're looking at the additionItems schema object in a tuple-typed array schema } else if (typeof schema.items === 'object') { - return (schema.items as JSONSchemaObjectType); // we're looking at the item schema for a non-tuple-typed array + return (schema.items as S); // we're looking at the single item schema object in a non-tuple-typed array schema } return schema; // schema is not an array } return pipe( updatedSchema, - schema => { - console.log("HERE1") - const x = filterRequiredHelper(getCorrectSchema(schema), getCorrectSchema(originalSchema)) - console.log("HERE2", x) - return x - }, + schema => filterRequiredHelper(getCorrectSchema(schema), getCorrectSchema(originalSchema)), O.map(required => { - // console.log("UPDATED SCHEMA", updatedSchema, "REQUIRED", required) - // console.log("Array.isArray(updatedSchema.items)", updatedSchema.items, Array.isArray(updatedSchema.items), updatedSchema.additionalItems, typeof updatedSchema.additionalItems === 'object') if (Array.isArray(updatedSchema.items) && typeof updatedSchema.additionalItems === 'object') { return { ...updatedSchema, @@ -208,8 +153,7 @@ const buildSchemaFilter = ( required: required } }; - } - else if (updatedSchema.items && typeof updatedSchema.items === 'object') { + } else if (updatedSchema.items && typeof updatedSchema.items === 'object') { return { ...updatedSchema, items: { @@ -217,7 +161,7 @@ const buildSchemaFilter = ( required: required, }, }; - } + } return { ...updatedSchema, required: required, @@ -227,93 +171,85 @@ const buildSchemaFilter = ( ); } + // Helper function to filter required properties from a list of objects function filterRequiredFromObjectsList(updatedSchema: S, originalSchema: S): Option { return pipe( - O.fromNullable(updatedSchema.items as JSONSchemaArrType), - O.chain(itemSchemas => { - return pipe( - O.fromNullable(originalSchema.items as JSONSchemaArrType), - O.map(originalItemSchemas => { - // console.log("ITEM SCHEMAS", itemSchemas, "ORIGINAL ITEM SCHEMAS", originalSchema.items) - return A.zip(itemSchemas, originalItemSchemas)}), - O.map(zippedSchemas => { - // console.log("HERE", zippedSchemas) - return zippedSchemas.map(([itemSchema, originalItemSchema]) => filterRequiredHelper(itemSchema, originalItemSchema)) - } - ) + O.fromNullable(updatedSchema.items as S[]), + O.chain(itemSchemas => pipe( + O.fromNullable(originalSchema.items as S[]), + O.map(originalItemSchemas => A.zip(itemSchemas, originalItemSchemas)), + O.map(zippedSchemas => + zippedSchemas.map(([itemSchema, originalItemSchema]) => filterRequiredHelper(itemSchema, originalItemSchema)) ) - }), + )), O.map(requiredList => { - // console.log("REQUIRED LIST", requiredList) const items = pipe( - A.zip(updatedSchema.items as JSONSchemaArrType, requiredList), + A.zip(updatedSchema.items as S[], requiredList), A.map(([item, required]) => { - // console.log("ITEM", item, "REQUIRED", required) - return { - ...item, - required: pipe( - required, - O.getOrElse(() => [] as string[]) - ) - } - } - ) + return { + ...item, + required: pipe( + required, + O.getOrElse(() => [] as string[]) + ) + }; + }) ); - return { ...updatedSchema, items: [ ...items - ], - } + ] + }; }), O.alt(() => O.some(updatedSchema)) ); } + function filterRequiredHelper(updatedSchema: S, originalSchema: S): Option { + return pipe ( + updatedSchema, + O.fromPredicate(schema => Array.isArray(schema.required)), + O.map(schema => Object.keys(schema.properties || {})), + O.map(updatedProperties => { + const originalPropertyNames = Object.keys(originalSchema.properties || {}); + return originalPropertyNames.filter(name => !updatedProperties.includes(name)); + }), + O.map(removedProperties => { + const required = originalSchema.required; + return (required as string[]).filter(name => !removedProperties.includes(name)); + }) + ); + } + + // Handles both non-array schemas and array schemas (both single-typed and tuple typed, with and without additionalItems) + // See: https://json-schema.org/understanding-json-schema/reference/array function filter(inputSchema: S): Option { return pipe( inputSchema, keepPropertyPredicate, - O.chain(inputSchema => { - - return pipe( - O.fromNullable(inputSchema), - O.chain(schema => - Array.isArray(schema.items) ? - pipe( - filterPropertiesFromObjectsList(schema), - O.alt(() => O.some(schema)), - O.chain(filteredSchema => { - // console.log("HEREEEE", filteredSchema, filteredSchema.items) - return filterPropertiesFromObjectSingle(filteredSchema) - }) - ) : - filterPropertiesFromObjectSingle(schema) - ) - ); - } ), - O.chain(schema => { - return pipe( - O.fromNullable(schema), - O.chain(schema => - Array.isArray(schema.items) ? - pipe( - filterRequiredFromObjectsList(schema, inputSchema), - O.alt(() => O.some(schema)), - O.chain(filteredSchema => { - // console.log("HEREEEE", filteredSchema, filteredSchema.items) - return filterRequiredFromObjectSingle(filteredSchema, inputSchema) - }) - ) : - filterRequiredFromObjectSingle(schema, inputSchema) - ) - ); - // if (inputSchema.items && Array.isArray(inputSchema.items)) { // Tuple typing - // return filterRequiredFromObjectsList(schema, inputSchema) - // } - // return filterRequiredFromObjectSingle(schema, inputSchema) - }) + O.chain(inputSchema => pipe( + O.fromNullable(inputSchema), + O.chain(schema => + Array.isArray(schema.items) ? // Schema is tuple-typed array + pipe( + filterPropertiesFromObjectsList(schema), // Filter properties from the tuple-typed list of item schema objects + O.chain(filteredSchema => filterPropertiesFromObjectSingle(filteredSchema)) // Try to filter properties from additionalItems, if it exists + ) + : filterPropertiesFromObjectSingle(schema) // Schema is non-tuple-typed array, or a non-array schema. Either way we're filtering properties from a single object. + ) + )), + O.chain(schema => pipe( + O.fromNullable(schema), + O.chain(schema => + Array.isArray(schema.items) ? // Schema is tuple-typed array + pipe( + filterRequiredFromObjectsList(schema, inputSchema), // Filter required from the tuple-typed list of item schema objects + O.chain(filteredSchema => filterRequiredFromObjectSingle(filteredSchema, inputSchema)) // Try to filter required from additionalItems, if it exists + ) + : filterRequiredFromObjectSingle(schema, inputSchema) // Schema is non-tuple-typed array, or a non-array schema. Either way we're filtering required from a single object. + ) + )) ); } From d83867002d8f46697384e8330697405c5f7f3060 Mon Sep 17 00:00:00 2001 From: Ilana Shapiro Date: Fri, 19 Apr 2024 16:21:27 -0700 Subject: [PATCH 11/11] add writeOnly tests --- .../filterRequiredProperties.test.ts | 243 +++++++++++++++++- 1 file changed, 237 insertions(+), 6 deletions(-) diff --git a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts index 41d018ce9..87f726d46 100644 --- a/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts +++ b/packages/http/src/utils/__tests__/filterRequiredProperties.test.ts @@ -92,6 +92,55 @@ describe('filterRequiredProperties', () => { }); }); + it('strips writeOnly properties from objects in single schema array', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + objectsArray: { + type: 'array', + items: { + type: 'object', + required: ['id', 'name'], + properties: { + id: { + writeOnly: true, + type: 'string' + }, + name: { + type: 'string' + } + } + } + }, + title: { type: 'string', writeOnly: true }, + address: { type: 'integer' }, + }, + required: ['title', 'address'], + }; + + assertSome(stripWriteOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + const arr_items = (schema.properties.objectsArray as JSONSchema).items as JSONSchema; + + expect(arr_items).not.toBeNull(); + expect(arr_items).not.toBeUndefined(); + + if (arr_items) { + expect(arr_items.required).toEqual(['name']); + expect(arr_items.properties).toEqual({ + name: expect.any(Object), + }); + expect(schema.required).toEqual(['address']); + expect(schema.properties).toEqual({ + address: expect.any(Object), + objectsArray: expect.any(Object), + }); + } + } + }); + }); + it('strips readOnly properties from objects in tuple-typed array and unspecified additionalItems', () => { const schema: JSONSchema = { type: 'object', @@ -102,12 +151,18 @@ describe('filterRequiredProperties', () => { { type: 'object', required: ['id', 'name'], - properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + properties: { + id: { readOnly: true, type: 'string' }, + name: { type: 'string' } + } }, { type: 'object', required: ['address', 'title'], - properties: { address: { readOnly: true, type: 'string' }, title: { type: 'string' } } + properties: { + address: { readOnly: true, type: 'string' }, + title: { type: 'string' } + } } ] }, @@ -136,6 +191,56 @@ describe('filterRequiredProperties', () => { }); }); + it('strips writeOnly properties from objects in tuple-typed array and unspecified additionalItems', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + objectsArrayAdditionalItemsUnspecified: { + type: 'array', + items: [ + { + type: 'object', + required: ['id', 'name'], + properties: { + id: { writeOnly: true, type: 'string' }, + name: { type: 'string' } + } + }, + { + type: 'object', + required: ['address', 'title'], + properties: { + address: { writeOnly: true, type: 'string' }, + title: { type: 'string' } + } + } + ] + }, + } + }; + + assertSome(stripWriteOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + const arr_items = (schema.properties.objectsArrayAdditionalItemsUnspecified as JSONSchema).items; + + expect(arr_items).not.toBeNull(); + expect(arr_items).not.toBeUndefined(); + + if (arr_items) { + expect(arr_items[0].required).toEqual(['name']); + expect(arr_items[0].properties).toEqual({ + name: expect.any(Object), + }); + expect(arr_items[1].required).toEqual(['title']); + expect(arr_items[1].properties).toEqual({ + title: expect.any(Object), + }); + } + } + }); + }); + it('strips readOnly properties from objects in tuple-typed array with additionalItems', () => { const schema: JSONSchema = { type: 'object', @@ -146,12 +251,18 @@ describe('filterRequiredProperties', () => { { type: 'object', required: ['id', 'name'], - properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + properties: { + id: { readOnly: true, type: 'string' }, + name: { type: 'string' } + } }, { type: 'object', required: ['address', 'title'], - properties: { address: { readOnly: true, type: 'string' }, title: { type: 'string' } } + properties: { + address: { readOnly: true, type: 'string' }, + title: { type: 'string' } + } } ], additionalItems: { @@ -194,6 +305,70 @@ describe('filterRequiredProperties', () => { }); }); + it('strips writeOnly properties from objects in tuple-typed array with additionalItems', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + objectsArrayWithAdditionalItems: { + type: 'array', + items: [ + { + type: 'object', + required: ['id', 'name'], + properties: { + id: { writeOnly: true, type: 'string' }, + name: { type: 'string' } + } + }, + { + type: 'object', + required: ['address', 'title'], + properties: { + address: { writeOnly: true, type: 'string' }, + title: { type: 'string' } + } + } + ], + additionalItems: { + type: 'object', + properties: { + status: { writeOnly: true, type: 'string' }, + ticket: { type: 'string' } + }, + required: ['status', 'ticket'] + } + } + } + }; + + assertSome(stripWriteOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + const arr = schema.properties.objectsArrayWithAdditionalItems as JSONSchema; + + expect(arr.items).not.toBeNull(); + expect(arr.items).not.toBeUndefined(); + + const arr_items = arr.items as JSONSchema; + expect(arr_items[0].required).toEqual(['name']); + expect(arr_items[0].properties).toEqual({ + name: expect.any(Object) + }); + expect(arr_items[1].required).toEqual(['title']); + expect(arr_items[1].properties).toEqual({ + title: expect.any(Object) + }); + + expect(arr.additionalItems).not.toBeNull(); + const additional_items = arr.additionalItems as JSONSchema + expect(additional_items.properties).toEqual({ + ticket: expect.any(Object) + }); + expect(additional_items.required).toEqual(['ticket']); + } + }); + }); + it('strips readOnly properties from objects in tuple-typed array no additionalItems', () => { const schema: JSONSchema = { type: 'object', @@ -204,12 +379,18 @@ describe('filterRequiredProperties', () => { { type: 'object', required: ['id', 'name'], - properties: { id: { readOnly: true, type: 'string' }, name: { type: 'string' } } + properties: { + id: { readOnly: true, type: 'string' }, + name: { type: 'string' } + } }, { type: 'object', required: ['address', 'title'], - properties: { address: { readOnly: true, type: 'string' }, title: { type: 'string' } } + properties: { + address: { readOnly: true, type: 'string' }, + title: { type: 'string' } + } } ], additionalItems: false @@ -240,6 +421,56 @@ describe('filterRequiredProperties', () => { }); }); + it('strips writeOnly properties from objects in tuple-typed array no additionalItems', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + objectsArrayNoAdditionalItems: { + type: 'array', + items: [ + { + type: 'object', + required: ['id', 'name'], + properties: { + id: { writeOnly: true, type: 'string' }, + name: { type: 'string' } } + }, + { + type: 'object', + required: ['address', 'title'], + properties: { + address: { writeOnly: true, type: 'string' }, + title: { type: 'string' } } + } + ], + additionalItems: false + }, + } + }; + + assertSome(stripWriteOnlyProperties(schema), schema => { + expect(schema.properties).not.toBeNull() + if (schema.properties) { + const arr = schema.properties.objectsArrayNoAdditionalItems as JSONSchema; + + expect(arr.items).not.toBeNull(); + expect(arr.items).not.toBeUndefined(); + + const arr_items = arr.items as JSONSchema; + expect(arr_items[0].required).toEqual(['name']); + expect(arr_items[0].properties).toEqual({ + name: expect.any(Object) + }); + expect(arr_items[1].required).toEqual(['title']); + expect(arr_items[1].properties).toEqual({ + title: expect.any(Object) + }); + + expect(arr.additionalItems).toEqual(false); + } + }); + }); + it('strips nested writeOnly properties', () => { const schema: JSONSchema = { type: 'object',