diff --git a/package-lock.json b/package-lock.json index e3ea6086..9351a7e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,7 @@ "rimraf": "^3.0.2", "stats-lite": "^2.2.0", "ts-node": "^10.8.2", - "typescript": "~5.4.5" + "typescript": "~5.5.4" }, "engines": { "node": ">=18.0.0", @@ -6587,9 +6587,9 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -11869,9 +11869,9 @@ } }, "typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true }, "unbox-primitive": { diff --git a/package.json b/package.json index 45eec7bc..da78df11 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "rimraf": "^3.0.2", "stats-lite": "^2.2.0", "ts-node": "^10.8.2", - "typescript": "~5.4.5" + "typescript": "~5.5.4" }, "dependencies": { "@graphql-tools/schema": "^8.5.0", diff --git a/spec/model/implementation/i18n-fields.spec.ts b/spec/model/implementation/i18n-fields.spec.ts index 66a5c21e..010892eb 100644 --- a/spec/model/implementation/i18n-fields.spec.ts +++ b/spec/model/implementation/i18n-fields.spec.ts @@ -5,7 +5,7 @@ const FULLY = 'fully'; const PARTIALLY = 'partially'; const NAMESPACED = 'namespaced'; -const i18n: LocalizationConfig[] = [ +const i18n: ReadonlyArray = [ { language: FULLY, namespacePath: [], diff --git a/spec/model/implementation/i18n-types.spec.ts b/spec/model/implementation/i18n-types.spec.ts index f31156e0..84c041b8 100644 --- a/spec/model/implementation/i18n-types.spec.ts +++ b/spec/model/implementation/i18n-types.spec.ts @@ -5,7 +5,7 @@ const FULLY = 'fully'; const PARTIALLY = 'partially'; const NAMESPACED = 'namespaced'; -const i18n: LocalizationConfig[] = [ +const i18n: ReadonlyArray = [ { language: FULLY, namespacePath: [], diff --git a/spec/performance/associations.perf.ts b/spec/performance/associations.perf.ts index ad446dc5..89a79623 100644 --- a/spec/performance/associations.perf.ts +++ b/spec/performance/associations.perf.ts @@ -48,7 +48,7 @@ function testFetchWithAssociations(config: { associationCount: number; }): BenchmarkConfig { let env: TestEnvironment; - let sampledIDs: string[] = []; + let sampledIDs: ReadonlyArray = []; return { name: `Fetch one of one root entity with one level deep associations (${config.paperCount} papers, ${config.userCount} users, ${config.associationCount} associations`, async beforeAll() { diff --git a/spec/performance/crud.perf.ts b/spec/performance/crud.perf.ts index 3b884188..243db91b 100644 --- a/spec/performance/crud.perf.ts +++ b/spec/performance/crud.perf.ts @@ -42,7 +42,7 @@ function getOneOfXRootEntities(config: { onlyFewFields?: boolean; }): BenchmarkConfig { let env: TestEnvironment; - let sampledIDs: string[] = []; + let sampledIDs: ReadonlyArray = []; const sizeFactor = getSizeFactorForJSONLength(config.documentLength); return { name: `Get ${config.onlyFewFields ? 'two fields of ' : ''} one of ${ diff --git a/spec/performance/query-pipeline.perf.ts b/spec/performance/query-pipeline.perf.ts index 7acd4102..17d2ca55 100644 --- a/spec/performance/query-pipeline.perf.ts +++ b/spec/performance/query-pipeline.perf.ts @@ -160,7 +160,7 @@ function testQueryPipeline(params: { let schema: GraphQLSchema; let model: Model; - let preparedQueries: PreparedQuery[]; + let preparedQueries: ReadonlyArray; return { name: `Run query pipeline with ${optionsStr}`, diff --git a/spec/performance/references.perf.ts b/spec/performance/references.perf.ts index f5aa0dc1..b7c61806 100644 --- a/spec/performance/references.perf.ts +++ b/spec/performance/references.perf.ts @@ -36,7 +36,7 @@ function testReferenceLookup(config: { referenceCountEach: number; }): BenchmarkConfig { let env: TestEnvironment; - let sampledIDs: string[] = []; + let sampledIDs: ReadonlyArray = []; return { name: `Set up ${config.paperCount} root entities with ${config.referenceCountEach} references each, then fetch a random root entity with all its references`, async beforeAll() { diff --git a/spec/performance/support/async-bench.ts b/spec/performance/support/async-bench.ts index fd7504e0..f5a9300c 100644 --- a/spec/performance/support/async-bench.ts +++ b/spec/performance/support/async-bench.ts @@ -88,7 +88,7 @@ export interface BenchmarkResultConfig { readonly elapsedTime: number; readonly setUpTime: number; - readonly cycleDetails: BenchmarkCycleDetails[]; + readonly cycleDetails: ReadonlyArray; } export class BenchmarkResult { @@ -100,7 +100,7 @@ export class BenchmarkResult { /** * Detailed information about each cycle */ - public readonly cycleDetails: BenchmarkCycleDetails[]; + public readonly cycleDetails: ReadonlyArray; /** * The mean time, in seconds, per iteration @@ -154,7 +154,9 @@ export async function benchmark( config: BenchmarkConfig, callbacks?: BenchmarkExecutionCallbacks, ): Promise { - async function cycle(count: number): Promise<{ times: number[]; netTime: number }> { + async function cycle( + count: number, + ): Promise<{ times: ReadonlyArray; netTime: number }> { if (config.before) { await config.before({ count }); } @@ -171,7 +173,9 @@ export async function benchmark( }; } - async function cycleSync(count: number): Promise<{ times: number[]; netTime: number }> { + async function cycleSync( + count: number, + ): Promise<{ times: ReadonlyArray; netTime: number }> { if (config.before) { await config.before({ count }); } @@ -188,7 +192,9 @@ export async function benchmark( }; } - async function cycleDetailed(count: number): Promise<{ times: number[]; netTime: number }> { + async function cycleDetailed( + count: number, + ): Promise<{ times: ReadonlyArray; netTime: number }> { if (config.before) { await config.before({ count }); } @@ -359,7 +365,7 @@ const tTable: { [key: string]: number } = { infinity: 1.96, }; -function getTimings(times: number[]): Timings { +function getTimings(times: ReadonlyArray): Timings { const mean: number = stats.mean(times); // Compute the sample standard deviation (estimate of the population standard deviation). const sd = stats.stdev(times); diff --git a/spec/performance/support/helpers.ts b/spec/performance/support/helpers.ts index 972ce729..d1fa7164 100644 --- a/spec/performance/support/helpers.ts +++ b/spec/performance/support/helpers.ts @@ -10,7 +10,8 @@ import { Log4jsLoggerProvider } from '../../helpers/log4js-logger-provider'; import { createTempDatabase } from '../../regression/initialization'; // arangojs typings for this are completely broken -export const aql: (template: TemplateStringsArray, ...args: any[]) => any = require('arangojs').aql; +export const aql: (template: TemplateStringsArray, ...args: ReadonlyArray) => any = + require('arangojs').aql; const MODEL_PATH = resolve(__dirname, '../../regression/papers/model'); @@ -130,7 +131,7 @@ export async function addNumberedPapersWithAQL(environment: TestEnvironment, cou export async function getRandomPaperIDsWithAQL( environment: TestEnvironment, count: number, -): Promise { +): Promise> { const cursor = await environment .getDB() .query(aql`FOR node IN papers SORT RAND() LIMIT ${count} RETURN { id: node._key }`); diff --git a/spec/regression/initialization.ts b/spec/regression/initialization.ts index 0714b972..b4e2d89b 100644 --- a/spec/regression/initialization.ts +++ b/spec/regression/initialization.ts @@ -16,7 +16,7 @@ export async function createTempDatabase(): Promise { const dbs = await systemDatabase.listDatabases(); if (dbs.indexOf(DATABASE_NAME) >= 0) { const db = systemDatabase.database(DATABASE_NAME); - const colls = (await db.collections(true)) as Collection[]; + const colls = await db.collections(true); await Promise.all(colls.map((coll) => coll.drop())); } else { await systemDatabase.createDatabase(DATABASE_NAME); @@ -144,7 +144,7 @@ export async function initTestData( return { fillTemplateStrings }; } -function wrapNamespaceForQuery(stuff: string, namespace: string[]) { +function wrapNamespaceForQuery(stuff: string, namespace: ReadonlyArray) { if (!namespace) { return stuff; } @@ -155,7 +155,7 @@ function wrapNamespaceForQuery(stuff: string, namespace: string[]) { return result; } -function retrieveIdFromResult(result: ExecutionResult, namespace: string[]) { +function retrieveIdFromResult(result: ExecutionResult, namespace: ReadonlyArray) { const ns = [...namespace]; let node = result.data as any; while (ns.length) { diff --git a/spec/regression/regression-suite.ts b/spec/regression/regression-suite.ts index ace3b11a..bfe151a6 100644 --- a/spec/regression/regression-suite.ts +++ b/spec/regression/regression-suite.ts @@ -203,7 +203,7 @@ export class RegressionSuite { const operations = parse(gqlSource).definitions.filter( (def) => def.kind == 'OperationDefinition', - ) as OperationDefinitionNode[]; + ) as ReadonlyArray; this._isSetUpClean = this._isSetUpClean && !operations.some((op) => op.operation == 'mutation'); const hasNamedOperations = operations.length && operations[0].name; diff --git a/spec/schema/source-validation-modules/sidecar-schema.spec.ts b/spec/schema/source-validation-modules/sidecar-schema.spec.ts index e95c5ae1..c2bf8623 100644 --- a/spec/schema/source-validation-modules/sidecar-schema.spec.ts +++ b/spec/schema/source-validation-modules/sidecar-schema.spec.ts @@ -63,7 +63,7 @@ const invalidValueWithComments = `{ describe('sidecar-schema validator', () => { const validator = new SidecarSchemaValidator(); - function getValidatorMessages(ps: ProjectSource): ValidationMessage[] { + function getValidatorMessages(ps: ProjectSource): ReadonlyArray { const parsedSource = parseProjectSource(ps, {}, new ValidationContext()); if (parsedSource) { return validator.validate(parsedSource); diff --git a/spec/schema/source-validation-modules/source-validation-helper.ts b/spec/schema/source-validation-modules/source-validation-helper.ts index 1781aef3..5c07b90a 100644 --- a/spec/schema/source-validation-modules/source-validation-helper.ts +++ b/spec/schema/source-validation-modules/source-validation-helper.ts @@ -2,7 +2,7 @@ import { ValidationContext, ValidationMessage } from '../../../src/model/validat import { ProjectSource } from '../../../src/project/source'; import { parseProjectSource } from '../../../src/schema/schema-builder'; -export function getMessages(source: ProjectSource): ValidationMessage[] { +export function getMessages(source: ProjectSource): ReadonlyArray { const validationContext = new ValidationContext(); parseProjectSource(source, {}, validationContext); diff --git a/src/authorization/execution.ts b/src/authorization/execution.ts index f226b06d..e079c121 100644 --- a/src/authorization/execution.ts +++ b/src/authorization/execution.ts @@ -14,7 +14,7 @@ import { AuthContext } from './auth-basics'; import { moveErrorsToOutputNodes } from './move-errors-to-output-nodes'; import { transformNode } from './transformers'; -const MUTATIONS: Function[] = [ +const MUTATIONS: ReadonlyArray = [ CreateEntityQueryNode, UpdateEntitiesQueryNode, DeleteEntitiesQueryNode, diff --git a/src/authorization/permission-descriptors.ts b/src/authorization/permission-descriptors.ts index 8b9ff4d9..d2bbdc17 100644 --- a/src/authorization/permission-descriptors.ts +++ b/src/authorization/permission-descriptors.ts @@ -14,6 +14,7 @@ import { simplifyBooleans } from '../query-tree/utils'; import { createFieldPathNode } from '../schema-generation/field-path-node'; import { ACCESS_GROUP_FIELD } from '../schema/constants'; import { AccessOperation, AuthContext } from './auth-basics'; +import { isReadonlyArray } from '../utils/utils'; export enum ConditionExplanationContext { BEFORE_WRITE, @@ -248,7 +249,7 @@ export class ProfileBasedPermissionDescriptor extends PermissionDescriptor { if (restriction.claim !== undefined) { const claimValue = authContext.claims?.[restriction.claim]; - const claimValues = Array.isArray(claimValue) ? claimValue : [claimValue]; + const claimValues = isReadonlyArray(claimValue) ? claimValue : [claimValue]; const sanitizedClaimValues = claimValues.filter( (v) => !!v && typeof v === 'string', ); diff --git a/src/config/parsed-project.ts b/src/config/parsed-project.ts index 681da74c..2be9ee8d 100644 --- a/src/config/parsed-project.ts +++ b/src/config/parsed-project.ts @@ -3,7 +3,7 @@ import { PlainObject } from '../utils/utils'; import { MessageLocation } from '../model/validation/message'; export interface ParsedProject { - readonly sources: ParsedProjectSource[]; + readonly sources: ReadonlyArray; } export type ParsedProjectSource = ParsedGraphQLProjectSource | ParsedObjectProjectSource; diff --git a/src/database/arangodb/aql-generator.ts b/src/database/arangodb/aql-generator.ts index 2e3da23c..a011e8f1 100644 --- a/src/database/arangodb/aql-generator.ts +++ b/src/database/arangodb/aql-generator.ts @@ -71,7 +71,7 @@ import { import { Quantifier, QuantifierFilterNode } from '../../query-tree/quantifiers'; import { extractVariableAssignments, simplifyBooleans } from '../../query-tree/utils'; import { not } from '../../schema-generation/utils/input-types'; -import { Constructor, decapitalize } from '../../utils/utils'; +import { Constructor, decapitalize, isReadonlyArray } from '../../utils/utils'; import { FlexSearchTokenizable } from '../database-adapter'; import { analyzeLikePatternPrefix } from '../like-helpers'; import { @@ -286,7 +286,7 @@ class QueryContext { return variable; } - getPreExecuteQueries(): AQLCompoundQuery[] { + getPreExecuteQueries(): ReadonlyArray { return this.preExecQueries; } @@ -1862,7 +1862,7 @@ function getFullIDsFromKeysNode( } if ( idsNode instanceof LiteralQueryNode && - Array.isArray(idsNode.value) && + isReadonlyArray(idsNode.value) && idsNode.value.every((v) => typeof v === 'string') ) { const collName = getCollectionNameForRootEntity(rootEntityType); diff --git a/src/database/arangodb/aql.ts b/src/database/arangodb/aql.ts index a3dc9597..8d4a0eb7 100644 --- a/src/database/arangodb/aql.ts +++ b/src/database/arangodb/aql.ts @@ -26,7 +26,7 @@ function indentLineBreaks(val: string, level: number) { } export class AQLCodeBuildingContext { - private readonly boundValues: any[] = []; + private readonly boundValues: unknown[] = []; private readonly boundCollectionNames = new Set(); private variableBindings = new Map(); private preExecInjectedVariablesMap = new Map(); @@ -340,7 +340,10 @@ export namespace aql { return new AQLCompoundFragment(fragments); } - export function join(fragments: AQLFragment[], separator: AQLFragment): AQLFragment { + export function join( + fragments: ReadonlyArray, + separator: AQLFragment, + ): AQLFragment { const newFragments: AQLFragment[] = []; let isFirst = true; for (const fragment of fragments) { @@ -360,7 +363,7 @@ export namespace aql { return new AQLCodeFragment(code); } - export function lines(...fragments: AQLFragment[]) { + export function lines(...fragments: ReadonlyArray) { return join(fragments, aql`\n`); } @@ -424,7 +427,7 @@ export namespace aql { */ export class AQLCompoundQuery extends AQLFragment { constructor( - public readonly preExecQueries: AQLCompoundQuery[], + public readonly preExecQueries: ReadonlyArray, public readonly aqlQuery: AQLFragment, public readonly resultVar: AQLQueryResultVariable | undefined, public readonly resultValidator: QueryResultValidator | undefined, @@ -439,14 +442,14 @@ export class AQLCompoundQuery extends AQLFragment { * * The returned transaction steps are to be executed sequentially. */ - getExecutableQueries(): AQLExecutableQuery[] { + getExecutableQueries(): ReadonlyArray { const resultVarToNameMap = new Map(); return this.getExecutableQueriesRecursive(resultVarToNameMap); } private getExecutableQueriesRecursive( resultVarToNameMap: Map, - ): AQLExecutableQuery[] { + ): ReadonlyArray { const executableQueries = flatMap(this.preExecQueries, (aqlQuery) => aqlQuery.getExecutableQueriesRecursive(resultVarToNameMap), ); diff --git a/src/database/arangodb/arangodb-adapter.ts b/src/database/arangodb/arangodb-adapter.ts index 113a032f..723a60a7 100644 --- a/src/database/arangodb/arangodb-adapter.ts +++ b/src/database/arangodb/arangodb-adapter.ts @@ -328,7 +328,7 @@ export class ArangoDBAdapter implements DatabaseAdapter { async executeExt({ queryTree, ...options }: ExecutionArgs): Promise { const prepStartTime = getPreciseTime(); globalContext.registerContext(this.schemaContext); - let executableQueries: AQLExecutableQuery[]; + let executableQueries: ReadonlyArray; let aqlQuery: AQLCompoundQuery; const oldEnableIndentationForCode = aqlConfig.enableIndentationForCode; aqlConfig.enableIndentationForCode = !!options.recordPlan; diff --git a/src/database/arangodb/arangojs-instrumentation/custom-connection.ts b/src/database/arangodb/arangojs-instrumentation/custom-connection.ts index 8315cc42..89f49c8b 100644 --- a/src/database/arangodb/arangojs-instrumentation/custom-connection.ts +++ b/src/database/arangodb/arangojs-instrumentation/custom-connection.ts @@ -3,6 +3,7 @@ import { normalizeUrl } from 'arangojs/lib/normalizeUrl'; import { ArangojsResponse } from 'arangojs/lib/request'; import { RequestInstrumentation, requestInstrumentationBodyKey } from './config'; import { createRequest, RequestOptions as CustomRequestOptions } from './custom-request'; +import { isReadonlyArray } from '../../../utils/utils'; /** * @internal @@ -111,8 +112,8 @@ export class CustomConnection extends Connection { }); } - addToHostList(urls: string | string[]): string[] { - const cleanUrls = (Array.isArray(urls) ? urls : [urls]).map((url) => normalizeUrl(url)); + addToHostList(urls: string | ReadonlyArray): string[] { + const cleanUrls = (isReadonlyArray(urls) ? urls : [urls]).map((url) => normalizeUrl(url)); const newUrls = cleanUrls.filter((url) => this._hostUrls.indexOf(url) === -1); this._hostUrls.push(...newUrls); this._hosts.push( diff --git a/src/database/inmemory/inmemory-adapter.ts b/src/database/inmemory/inmemory-adapter.ts index 5063b59a..814e855d 100644 --- a/src/database/inmemory/inmemory-adapter.ts +++ b/src/database/inmemory/inmemory-adapter.ts @@ -16,9 +16,10 @@ import { JSCompoundQuery, JSExecutableQuery } from './js'; import { getJSQuery } from './js-generator'; import { v4 as uuid } from 'uuid'; import { DefaultClock, IDGenerator, UUIDGenerator } from '../../execution/execution-options'; +import { isReadonlyArray } from '../../utils/utils'; export class InMemoryDB { - collections: { [name: string]: any[] } = {}; + collections: { [name: string]: ReadonlyArray } = {}; } export class InMemoryAdapter implements DatabaseAdapter { @@ -42,7 +43,7 @@ export class InMemoryAdapter implements DatabaseAdapter { * @returns {string} */ private executeQueries( - queries: JSExecutableQuery[], + queries: ReadonlyArray, { idGenerator }: { idGenerator: IDGenerator }, ) { const validators = new Map( @@ -97,8 +98,8 @@ export class InMemoryAdapter implements DatabaseAdapter { return 1; } - if (Array.isArray(lhs)) { - if (!Array.isArray(rhs)) { + if (isReadonlyArray(lhs)) { + if (!isReadonlyArray(rhs)) { return -1; } @@ -112,7 +113,7 @@ export class InMemoryAdapter implements DatabaseAdapter { } } } - if (Array.isArray(rhs)) { + if (isReadonlyArray(rhs)) { return 1; } @@ -196,7 +197,7 @@ export class InMemoryAdapter implements DatabaseAdapter { likePatternToRegExp, ensureArray: (arg: unknown) => { - if (Array.isArray(arg)) { + if (isReadonlyArray(arg)) { return arg; } return [arg]; @@ -262,7 +263,7 @@ export class InMemoryAdapter implements DatabaseAdapter { async executeExt(args: ExecutionArgs): Promise { globalContext.registerContext(this.schemaContext); - let executableQueries: JSExecutableQuery[]; + let executableQueries: ReadonlyArray; let jsQuery: JSCompoundQuery; try { jsQuery = getJSQuery(args.queryTree, { diff --git a/src/database/inmemory/js-generator.ts b/src/database/inmemory/js-generator.ts index b61f7d3d..e2109d6c 100644 --- a/src/database/inmemory/js-generator.ts +++ b/src/database/inmemory/js-generator.ts @@ -192,7 +192,7 @@ class QueryContext { return js`${variable}`; } - getPreExecuteQueries(): JSCompoundQuery[] { + getPreExecuteQueries(): ReadonlyArray { return this.preExecQueries; } } @@ -220,7 +220,7 @@ namespace jsExt { } } - export function executingFunction(...content: JSFragment[]): JSFragment { + export function executingFunction(...content: ReadonlyArray): JSFragment { return js.lines(js`(function() {`, js.indent(js.lines(...content)), js`})()`); } @@ -1339,7 +1339,7 @@ function processNode(node: QueryNode, context: QueryContext): JSFragment { } // TODO I think JSCompoundQuery (JS transaction node) should not be the exported type -// we should rather export JSExecutableQuery[] (as JS transaction) directly. +// we should rather export ReadonlyArray (as JS transaction) directly. export function getJSQuery( node: QueryNode, options: Partial = {}, diff --git a/src/database/inmemory/js.ts b/src/database/inmemory/js.ts index daa3090f..6e444667 100644 --- a/src/database/inmemory/js.ts +++ b/src/database/inmemory/js.ts @@ -22,7 +22,7 @@ function indentLineBreaks(val: string, level: number) { } export class JSCodeBuildingContext { - private boundValues: any[] = []; + private boundValues: unknown[] = []; private variableBindings = new Map(); private preExecInjectedVariablesMap = new Map(); private nextIndexPerLabel = new Map(); @@ -180,7 +180,7 @@ export class JSBoundValue extends JSFragment { } export class JSCompoundFragment extends JSFragment { - constructor(public readonly fragments: JSFragment[]) { + constructor(public readonly fragments: ReadonlyArray) { super(); } @@ -271,7 +271,7 @@ export function js( } export namespace js { - export function join(fragments: JSFragment[], separator: JSFragment): JSFragment { + export function join(fragments: ReadonlyArray, separator: JSFragment): JSFragment { const newFragments: JSFragment[] = []; let isFirst = true; for (const fragment of fragments) { @@ -291,7 +291,7 @@ export namespace js { return new JSCodeFragment(code); } - export function lines(...fragments: JSFragment[]) { + export function lines(...fragments: ReadonlyArray) { return join(fragments, js`\n`); } @@ -353,7 +353,7 @@ export namespace js { */ export class JSCompoundQuery extends JSFragment { constructor( - public readonly preExecQueries: JSCompoundQuery[], + public readonly preExecQueries: ReadonlyArray, public readonly jsQuery: JSFragment, public readonly resultVar: JSQueryResultVariable | undefined, public readonly resultValidator: QueryResultValidator | undefined, @@ -366,14 +366,14 @@ export class JSCompoundQuery extends JSFragment { * * The returned transaction steps are to be executed sequentially. */ - getExecutableQueries(): JSExecutableQuery[] { + getExecutableQueries(): ReadonlyArray { const resultVarToNameMap = new Map(); return this.getExecutableQueriesRecursive(resultVarToNameMap); } private getExecutableQueriesRecursive( resultVarToNameMap: Map, - ): JSExecutableQuery[] { + ): ReadonlyArray { const executableQueries = flatMap(this.preExecQueries, (JSQuery) => JSQuery.getExecutableQueriesRecursive(resultVarToNameMap), ); diff --git a/src/graphql/language-utils.ts b/src/graphql/language-utils.ts index 384528a4..b0ed1887 100644 --- a/src/graphql/language-utils.ts +++ b/src/graphql/language-utils.ts @@ -399,7 +399,7 @@ export function collectFieldNodesInPath( throw new Error(`Aliases must not be empty`); } - let currentSelectionSets: SelectionSetNode[] = [selectionSet]; + let currentSelectionSets: ReadonlyArray = [selectionSet]; const fieldNodesInPath: FieldNode[] = []; for (const alias of aliases) { if (!currentSelectionSets.length) { diff --git a/src/graphql/query-distiller.ts b/src/graphql/query-distiller.ts index 83724826..5da16246 100644 --- a/src/graphql/query-distiller.ts +++ b/src/graphql/query-distiller.ts @@ -231,7 +231,7 @@ function distillSelections( * Not supported are unions, interfaces and possibly other features. */ function buildFieldRequest( - fieldNodes: Array, + fieldNodes: ReadonlyArray, parentType: GraphQLCompositeType, context: Context, ): FieldRequest { diff --git a/src/model/create-model.ts b/src/model/create-model.ts index a0a1fc75..56cccac0 100644 --- a/src/model/create-model.ts +++ b/src/model/create-model.ts @@ -1181,7 +1181,7 @@ function extractBilling(parsedProject: ParsedProject): BillingConfig { ); } -function extractTimeToLive(parsedProject: ParsedProject): TimeToLiveConfig[] { +function extractTimeToLive(parsedProject: ParsedProject): ReadonlyArray { const objectSchemaParts = parsedProject.sources.filter( (parsedSource) => parsedSource.kind === ParsedProjectSourceBaseKind.OBJECT, ) as ReadonlyArray; diff --git a/src/model/implementation/permission-profile.ts b/src/model/implementation/permission-profile.ts index 6df6aed9..c335f1ec 100644 --- a/src/model/implementation/permission-profile.ts +++ b/src/model/implementation/permission-profile.ts @@ -1,6 +1,6 @@ import { AccessOperation, AuthContext } from '../../authorization/auth-basics'; import { WILDCARD_CHARACTER } from '../../schema/constants'; -import { escapeRegExp } from '../../utils/utils'; +import { escapeRegExp, isReadonlyArray } from '../../utils/utils'; import { MessageLocation, PermissionAccessKind, @@ -42,7 +42,7 @@ export class Permission implements ModelComponent { constructor(config: PermissionConfig) { this.roles = new RoleSpecifier(config.roles); - this.access = Array.isArray(config.access) ? config.access : [config.access]; + this.access = isReadonlyArray(config.access) ? config.access : [config.access]; this.restrictToAccessGroups = config.restrictToAccessGroups; this.hasDynamicAccessGroups = !!this.restrictToAccessGroups && diff --git a/src/model/parse-modules.ts b/src/model/parse-modules.ts index 39be5617..f3e51112 100644 --- a/src/model/parse-modules.ts +++ b/src/model/parse-modules.ts @@ -1,5 +1,6 @@ import { ModelOptions } from '../config/interfaces'; import { ParsedObjectProjectSource } from '../config/parsed-project'; +import { isReadonlyArray } from '../utils/utils'; import { ModuleConfig } from './config/module'; import { ValidationContext, ValidationMessage } from './validation'; @@ -8,7 +9,7 @@ export function parseModuleConfigs( options: ModelOptions, validationContext: ValidationContext, ): ReadonlyArray { - if (!source.object || !source.object.modules || !Array.isArray(source.object.modules)) { + if (!source.object || !source.object.modules || !isReadonlyArray(source.object.modules)) { return []; } diff --git a/src/model/parse-ttl.ts b/src/model/parse-ttl.ts index a2358f53..d5736741 100644 --- a/src/model/parse-ttl.ts +++ b/src/model/parse-ttl.ts @@ -1,11 +1,14 @@ import { ParsedObjectProjectSource } from '../config/parsed-project'; +import { isReadonlyArray } from '../utils/utils'; import { TimeToLiveConfig } from './config'; -export function parseTTLConfigs(source: ParsedObjectProjectSource): TimeToLiveConfig[] { - if (!source.object || !source.object.timeToLive || !Array.isArray(source.object.timeToLive)) { +export function parseTTLConfigs( + source: ParsedObjectProjectSource, +): ReadonlyArray { + if (!source.object || !source.object.timeToLive || !isReadonlyArray(source.object.timeToLive)) { return []; } - const ttlConfigs = source.object.timeToLive as TimeToLiveConfig[]; + const ttlConfigs = source.object.timeToLive as ReadonlyArray; return ttlConfigs.map((ttlConfig, index) => ({ ...ttlConfig, typeNameLoc: source.pathLocationMap[`/timeToLive/${index}/typeName`], diff --git a/src/model/validation/result.ts b/src/model/validation/result.ts index 99c38d0e..52faee97 100644 --- a/src/model/validation/result.ts +++ b/src/model/validation/result.ts @@ -1,7 +1,7 @@ import { Severity, ValidationMessage } from './message'; export class ValidationResult { - constructor(public readonly messages: ValidationMessage[]) {} + constructor(public readonly messages: ReadonlyArray) {} public hasMessages() { return this.messages.length > 0; @@ -31,6 +31,14 @@ export class ValidationResult { return this.messages.filter((message) => message.severity === Severity.INFO); } + public hasCompatibilityIssues() { + return this.messages.some((message) => message.severity === Severity.COMPATIBILITY_ISSUE); + } + + public getCompatibilityIssues() { + return this.messages.filter((message) => message.severity === Severity.COMPATIBILITY_ISSUE); + } + toString() { return this.messages.map((m) => m.toString()).join('\n'); } diff --git a/src/model/validation/validation-context.ts b/src/model/validation/validation-context.ts index af3744cc..1fc266fe 100644 --- a/src/model/validation/validation-context.ts +++ b/src/model/validation/validation-context.ts @@ -3,7 +3,7 @@ import { ValidationMessage, ValidationResult } from './index'; export class ValidationContext { private readonly _validationMessages: ValidationMessage[] = []; - addMessage(...messages: ValidationMessage[]) { + addMessage(...messages: ReadonlyArray) { messages.forEach((msg) => this._validationMessages.push(msg)); } diff --git a/src/project/project-from-fs.ts b/src/project/project-from-fs.ts index d6a3674e..88655f69 100644 --- a/src/project/project-from-fs.ts +++ b/src/project/project-from-fs.ts @@ -21,11 +21,11 @@ export async function loadProjectFromDir( async function loadSourcesFromDir( dirPath: string, parentSourcePath: string = '', -): Promise { - const fileNames: string[] = await readdir(dirPath); +): Promise> { + const fileNames: ReadonlyArray = await readdir(dirPath); return flatten(await Promise.all(fileNames.map(processFile))); - async function processFile(fileName: string): Promise { + async function processFile(fileName: string): Promise> { const sourcePath = concatSourcePaths(parentSourcePath, fileName); const filePath = resolve(dirPath, fileName); const stats = await stat(filePath); diff --git a/src/project/project.ts b/src/project/project.ts index 1250c959..5bf0cd77 100644 --- a/src/project/project.ts +++ b/src/project/project.ts @@ -15,6 +15,7 @@ import { createSchema, getModel, validateSchema } from '../schema/schema-builder import { ModuleSelectionOptions, selectModulesInProject } from './select-modules-in-sources'; import { ProjectSource, SourceLike, SourceType } from './source'; import { TTLInfo, getQueryNodeForTTLType, getTTLInfoQueryNode } from './time-to-live'; +import { isReadonlyArray } from '../utils/utils'; export { ProjectOptions }; @@ -107,10 +108,10 @@ export class Project { readonly options: ProjectOptions; - constructor(config: ProjectConfig | SourceLike[]) { - if (Array.isArray(config)) { - config = { sources: config }; - } + constructor(configOrSources: ProjectConfig | ReadonlyArray) { + const config: ProjectConfig = isReadonlyArray(configOrSources) + ? { sources: configOrSources } + : configOrSources; this.sources = config.sources.map((config) => ProjectSource.fromConfig(config)); this.loggerProvider = config.loggerProvider || DEFAULT_LOGGER_PROVIDER; this.options = { @@ -124,7 +125,7 @@ export class Project { }; } - getSourcesOfType(type: SourceType): ProjectSource[] { + getSourcesOfType(type: SourceType): ReadonlyArray { return this.sources.filter((source) => source.type == type); } diff --git a/src/project/select-modules-in-sources.ts b/src/project/select-modules-in-sources.ts index 849c0f7a..55993b90 100644 --- a/src/project/select-modules-in-sources.ts +++ b/src/project/select-modules-in-sources.ts @@ -20,6 +20,7 @@ import { parseProjectSource } from '../schema/schema-builder'; import { findDirectiveWithName } from '../schema/schema-utils'; import { Project, ProjectOptions } from './project'; import { ProjectSource } from './source'; +import { isReadonlyArray } from '../utils/utils'; export interface ModuleSelectionOptions { /** @@ -136,9 +137,11 @@ function selectModulesInObjectSource({ const newObject = { ...parsedSource.object }; if (removeModuleDeclarations) { delete newObject.modules; - } else if (Array.isArray(parsedSource.object.modules)) { + } else if (isReadonlyArray(parsedSource.object.modules)) { // if we shouldn't remove the module declarations completely, filter it down - newObject.modules = parsedSource.object.modules.filter((m) => selectedModules.has(m)); + newObject.modules = parsedSource.object.modules.filter( + (m) => selectedModules.has(m as string), // type assertion is safe for the .has() argument + ); } return JSON.stringify(newObject, undefined, ' '); } diff --git a/src/query-tree/utils/extract-variable-assignments.ts b/src/query-tree/utils/extract-variable-assignments.ts index 67e85e98..500d08de 100644 --- a/src/query-tree/utils/extract-variable-assignments.ts +++ b/src/query-tree/utils/extract-variable-assignments.ts @@ -97,7 +97,7 @@ export function extractVariableAssignments( */ export function prependVariableAssignments( node: QueryNode, - variableAssignmentsList: VariableAssignmentQueryNode[], + variableAssignmentsList: ReadonlyArray, ) { return variableAssignmentsList.reduce( (currentNode, assignmentNode) => diff --git a/src/schema-generation/create-input-types/generator.ts b/src/schema-generation/create-input-types/generator.ts index a30b1556..bba63258 100644 --- a/src/schema-generation/create-input-types/generator.ts +++ b/src/schema-generation/create-input-types/generator.ts @@ -79,7 +79,7 @@ export class CreateInputTypeGenerator { ); } - private generateFields(field: Field): CreateInputField[] { + private generateFields(field: Field): ReadonlyArray { if (field.isSystemField) { return []; } diff --git a/src/schema-generation/create-input-types/input-fields.ts b/src/schema-generation/create-input-types/input-fields.ts index 6198311e..fbfc0ff5 100644 --- a/src/schema-generation/create-input-types/input-fields.ts +++ b/src/schema-generation/create-input-types/input-fields.ts @@ -2,7 +2,7 @@ import { GraphQLInputType, GraphQLList, GraphQLNonNull } from 'graphql'; import { ZonedDateTime } from '@js-joda/core'; import { Field } from '../../model'; import { GraphQLOffsetDateTime, serializeForStorage } from '../../schema/scalars/offset-date-time'; -import { AnyValue, PlainObject } from '../../utils/utils'; +import { AnyValue, isReadonlyArray, PlainObject } from '../../utils/utils'; import { createGraphQLError } from '../graphql-errors'; import { FieldContext } from '../query-node-object-type'; import { TypedInputFieldBase, TypedInputObjectType } from '../typed-input-object-type'; @@ -112,7 +112,7 @@ export class BasicListCreateInputField extends BasicCreateInputField { protected coerceValue(value: AnyValue, context: FieldContext): AnyValue { // null is not a valid list value - if the user specified it, coerce it to [] to not have a mix of [] and // null in the database - let listValue = Array.isArray(value) ? value : []; + let listValue = isReadonlyArray(value) ? value : []; return listValue.map((itemValue) => super.coerceValue(itemValue, context)); } } @@ -190,12 +190,14 @@ export class ObjectListCreateInputField extends BasicCreateInputField { if (value === undefined) { return undefined; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value for "${this.name}" to be an array, but is "${typeof value}"`, ); } - return value.map((value) => this.objectInputType.prepareValue(value, context)); + return value.map((value) => + this.objectInputType.prepareValue(value as PlainObject, context), + ); } collectAffectedFields(value: AnyValue, fields: Set, context: FieldContext) { @@ -203,14 +205,14 @@ export class ObjectListCreateInputField extends BasicCreateInputField { if (value == undefined) { return; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value for "${this.name}" to be an array, but is "${typeof value}"`, ); } value.forEach((value) => - this.objectInputType.collectAffectedFields(value, fields, context), + this.objectInputType.collectAffectedFields(value as PlainObject, fields, context), ); } } diff --git a/src/schema-generation/create-input-types/relation-fields.ts b/src/schema-generation/create-input-types/relation-fields.ts index 865f8bd0..92e5da12 100644 --- a/src/schema-generation/create-input-types/relation-fields.ts +++ b/src/schema-generation/create-input-types/relation-fields.ts @@ -2,7 +2,7 @@ import { GraphQLID, GraphQLInputType, GraphQLList, GraphQLNonNull } from 'graphq import { Field, Multiplicity } from '../../model'; import { PreExecQueryParms, QueryNode } from '../../query-tree'; import { getCreateRelatedEntityFieldName } from '../../schema/names'; -import { AnyValue, PlainObject } from '../../utils/utils'; +import { AnyValue, isReadonlyArray, PlainObject } from '../../utils/utils'; import { FieldContext } from '../query-node-object-type'; import { getAddEdgesStatements, @@ -84,7 +84,7 @@ export class AddEdgesCreateInputField extends AbstractRelationCreateInputField { if (value == undefined) { return []; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value of "${this.name}" to be an array, but is ${typeof value}`, ); @@ -115,7 +115,7 @@ export class CreateAndAddEdgesCreateInputField extends AbstractRelationCreateInp if (value == undefined) { return []; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value of "${this.name}" to be an array, but is ${typeof value}`, ); @@ -152,7 +152,7 @@ export class CreateAndSetEdgeCreateInputField extends AbstractRelationCreateInpu if (value == undefined) { return []; } - if (Array.isArray(value)) { + if (isReadonlyArray(value)) { throw new Error( `Expected value of "${this.name}" to be an object, but is ${typeof value}`, ); diff --git a/src/schema-generation/filter-input-types/constants.ts b/src/schema-generation/filter-input-types/constants.ts index 81916e84..42635aac 100644 --- a/src/schema-generation/filter-input-types/constants.ts +++ b/src/schema-generation/filter-input-types/constants.ts @@ -142,7 +142,7 @@ export const FILTER_DESCRIPTIONS: { [name: string]: string | { [typeName: string export const OPERATORS_WITH_LIST_OPERAND = [INPUT_FIELD_IN, INPUT_FIELD_NOT_IN]; -export const FILTER_FIELDS_BY_TYPE: { [name: string]: string[] } = { +export const FILTER_FIELDS_BY_TYPE: { [name: string]: ReadonlyArray } = { [GraphQLString.name]: STRING_FILTER_FIELDS, [GraphQLID.name]: NUMERIC_FILTER_FIELDS, [GraphQLDateTime.name]: NUMERIC_FILTER_FIELDS, diff --git a/src/schema-generation/filter-input-types/filter-fields.ts b/src/schema-generation/filter-input-types/filter-fields.ts index 477709c5..0f63940b 100644 --- a/src/schema-generation/filter-input-types/filter-fields.ts +++ b/src/schema-generation/filter-input-types/filter-fields.ts @@ -22,7 +22,7 @@ import { OR_FILTER_FIELD, } from '../../schema/constants'; import { GraphQLOffsetDateTime, TIMESTAMP_PROPERTY } from '../../schema/scalars/offset-date-time'; -import { AnyValue, decapitalize, PlainObject } from '../../utils/utils'; +import { AnyValue, decapitalize, isReadonlyArray, PlainObject } from '../../utils/utils'; import { createFieldNode } from '../field-nodes'; import { TypedInputFieldBase } from '../typed-input-object-type'; import { FILTER_DESCRIPTIONS, OPERATORS_WITH_LIST_OPERAND, Quantifier } from './constants'; @@ -340,7 +340,7 @@ export class AndFilterField implements FilterField { } getFilterNode(sourceNode: QueryNode, filterValue: AnyValue): QueryNode { - if (!Array.isArray(filterValue) || !filterValue.length) { + if (!isReadonlyArray(filterValue) || !filterValue.length) { return new ConstBoolQueryNode(true); } const values = (filterValue || []) as ReadonlyArray; @@ -363,7 +363,7 @@ export class OrFilterField implements FilterField { } getFilterNode(sourceNode: QueryNode, filterValue: AnyValue): QueryNode { - if (!Array.isArray(filterValue)) { + if (!isReadonlyArray(filterValue)) { return new ConstBoolQueryNode(true); // regard as omitted } const values = filterValue as ReadonlyArray; diff --git a/src/schema-generation/filter-input-types/generator.ts b/src/schema-generation/filter-input-types/generator.ts index 1baa098c..c8c74750 100644 --- a/src/schema-generation/filter-input-types/generator.ts +++ b/src/schema-generation/filter-input-types/generator.ts @@ -143,7 +143,7 @@ export class FilterTypeGenerator { return []; } - private generateFilterFieldsForNonListScalar(field: Field): FilterField[] { + private generateFilterFieldsForNonListScalar(field: Field): ReadonlyArray { if (field.isList || !field.type.isScalarType) { throw new Error(`Expected "${field.name}" to be a non-list scalar`); } @@ -179,7 +179,7 @@ export class FilterTypeGenerator { private generateFilterFieldsForEnumField( field: Field, graphQLEnumType: GraphQLEnumType, - ): FilterField[] { + ): ReadonlyArray { if (field.isList || !field.type.isEnumType) { throw new Error(`Expected "${field.name}" to be a non-list enum`); } @@ -203,7 +203,7 @@ export class FilterTypeGenerator { return [...quantifierFields, new EmptyListFilterField(field)]; } - private buildScalarFilterFields(type: ScalarType): ScalarOrEnumFilterField[] { + private buildScalarFilterFields(type: ScalarType): ReadonlyArray { const filterFields = this.getFilterFieldsByType(type); return filterFields.map( (name) => diff --git a/src/schema-generation/flex-search-filter-input-types/constants.ts b/src/schema-generation/flex-search-filter-input-types/constants.ts index 6decf891..692760e6 100644 --- a/src/schema-generation/flex-search-filter-input-types/constants.ts +++ b/src/schema-generation/flex-search-filter-input-types/constants.ts @@ -106,7 +106,7 @@ const ID_FLEX_SEARCH_FILTER_FIELDS = [ INPUT_FIELD_NOT_IN, ]; -export const FLEX_SEARCH_FILTER_FIELDS_BY_TYPE: { [name: string]: string[] } = { +export const FLEX_SEARCH_FILTER_FIELDS_BY_TYPE: { [name: string]: ReadonlyArray } = { [GraphQLString.name]: STRING_FLEX_SEARCH_FILTER_FIELDS, [GraphQLID.name]: ID_FLEX_SEARCH_FILTER_FIELDS, [GraphQLDateTime.name]: NUMERIC_FILTER_FIELDS, diff --git a/src/schema-generation/flex-search-filter-input-types/filter-fields.ts b/src/schema-generation/flex-search-filter-input-types/filter-fields.ts index 9ab47cb0..d5b28fcc 100644 --- a/src/schema-generation/flex-search-filter-input-types/filter-fields.ts +++ b/src/schema-generation/flex-search-filter-input-types/filter-fields.ts @@ -36,7 +36,7 @@ import { INPUT_FIELD_STARTS_WITH, OR_FILTER_FIELD, } from '../../schema/constants'; -import { AnyValue, PlainObject } from '../../utils/utils'; +import { AnyValue, isReadonlyArray, PlainObject } from '../../utils/utils'; import { FilterField, getScalarFilterLiteralValue, @@ -367,7 +367,7 @@ export class FlexSearchAndFilterField implements FlexSearchFilterField { path: ReadonlyArray, info: QueryNodeResolveInfo, ): QueryNode { - if (!Array.isArray(filterValue) || !filterValue.length) { + if (!isReadonlyArray(filterValue) || !filterValue.length) { return new ConstBoolQueryNode(true); } const values = (filterValue || []) as ReadonlyArray; @@ -397,7 +397,7 @@ export class FlexSearchOrFilterField implements FlexSearchFilterField { path: ReadonlyArray, info: QueryNodeResolveInfo, ): QueryNode { - if (!Array.isArray(filterValue)) { + if (!isReadonlyArray(filterValue)) { return new ConstBoolQueryNode(true); // regard as omitted } const values = filterValue as ReadonlyArray; @@ -517,7 +517,7 @@ export function resolveFilterField( } if ( filterField.operatorName == INPUT_FIELD_IN && - Array.isArray(filterValue) && + isReadonlyArray(filterValue) && filterValue.includes(null) ) { return new BinaryOperationQueryNode( @@ -539,7 +539,7 @@ export function resolveFilterField( } if ( filterField.operatorName == INPUT_FIELD_NOT_IN && - Array.isArray(filterValue) && + isReadonlyArray(filterValue) && filterValue.includes(null) ) { return new BinaryOperationQueryNode( diff --git a/src/schema-generation/flex-search-filter-input-types/generator.ts b/src/schema-generation/flex-search-filter-input-types/generator.ts index c719a1b4..b657f7ed 100644 --- a/src/schema-generation/flex-search-filter-input-types/generator.ts +++ b/src/schema-generation/flex-search-filter-input-types/generator.ts @@ -318,7 +318,7 @@ export class FlexSearchFilterTypeGenerator { private generateFilterFieldsForNonListEnumField( field: Field, graphQLEnumType: GraphQLEnumType, - ): FlexSearchFilterField[] { + ): ReadonlyArray { if (field.isList || !field.type.isEnumType) { throw new Error(`Expected "${field.name}" to be a non-list enum`); } @@ -338,7 +338,7 @@ export class FlexSearchFilterTypeGenerator { private generateTypeSpecificListFieldFilterFields( field: Field, path?: ReadonlyArray, - ): FlexSearchFilterField[] { + ): ReadonlyArray { const pathParam = path ? path : []; if (field.type instanceof ScalarType) { return this.buildFilterFieldsForListScalar(field.type, field, pathParam); @@ -358,7 +358,7 @@ export class FlexSearchFilterTypeGenerator { type: ScalarType, field: Field, path?: ReadonlyArray, - ): FlexSearchScalarOrEnumFieldFilterField[] { + ): ReadonlyArray { const filterFields = this.getFilterFieldsByType(type); let scalarFields: FlexSearchScalarOrEnumFieldFilterField[] = []; diff --git a/src/schema-generation/flex-search-generator.ts b/src/schema-generation/flex-search-generator.ts index 4670a925..150232f6 100644 --- a/src/schema-generation/flex-search-generator.ts +++ b/src/schema-generation/flex-search-generator.ts @@ -267,7 +267,7 @@ export class FlexSearchGenerator { const expression = expressionParam; - function getQueryNodeFromField(field: Field, path: Field[] = []): QueryNode { + function getQueryNodeFromField(field: Field, path: ReadonlyArray = []): QueryNode { if (field.type.isObjectType) { return field.type.fields .filter((f) => f.isIncludedInSearch || f.isFulltextIncludedInSearch) diff --git a/src/schema-generation/mutation-type-generator.ts b/src/schema-generation/mutation-type-generator.ts index 2b47be62..fed2a21c 100644 --- a/src/schema-generation/mutation-type-generator.ts +++ b/src/schema-generation/mutation-type-generator.ts @@ -106,7 +106,7 @@ export class MutationTypeGenerator { }; } - private generateFields(rootEntityType: RootEntityType): QueryNodeField[] { + private generateFields(rootEntityType: RootEntityType): ReadonlyArray { const canCreatePluralFields = rootEntityType.name !== rootEntityType.pluralName; return compact([ diff --git a/src/schema-generation/update-input-types/generator.ts b/src/schema-generation/update-input-types/generator.ts index 0a16d222..281cf61e 100644 --- a/src/schema-generation/update-input-types/generator.ts +++ b/src/schema-generation/update-input-types/generator.ts @@ -106,7 +106,7 @@ export class UpdateInputTypeGenerator { skipID = false, skipRelations = false, }: { skipID?: boolean; skipRelations?: boolean } = {}, - ): UpdateInputField[] { + ): ReadonlyArray { if (field.isSystemField) { if ( !skipID && diff --git a/src/schema-generation/update-input-types/input-fields.ts b/src/schema-generation/update-input-types/input-fields.ts index c0a6931e..6eca3f7e 100644 --- a/src/schema-generation/update-input-types/input-fields.ts +++ b/src/schema-generation/update-input-types/input-fields.ts @@ -19,7 +19,7 @@ import { getUpdateChildEntitiesFieldName, } from '../../schema/names'; import { GraphQLOffsetDateTime, serializeForStorage } from '../../schema/scalars/offset-date-time'; -import { AnyValue, PlainObject } from '../../utils/utils'; +import { AnyValue, isReadonlyArray, PlainObject } from '../../utils/utils'; import { CreateChildEntityInputType, CreateObjectInputType } from '../create-input-types'; import { createFieldNode } from '../field-nodes'; import { FieldContext } from '../query-node-object-type'; @@ -136,7 +136,7 @@ export class BasicListUpdateInputField extends BasicUpdateInputField { protected coerceValue(value: AnyValue, context: FieldContext): AnyValue { // null is not a valid list value - if the user specified it, coerce it to [] to not have a mix of [] and // null in the database - let listValue = Array.isArray(value) ? value : []; + let listValue = isReadonlyArray(value) ? value : []; return listValue.map((itemValue) => super.coerceValue(itemValue, context)); } } @@ -225,12 +225,14 @@ export class UpdateValueObjectListInputField extends BasicUpdateInputField { if (value === undefined) { return undefined; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value for "${this.name}" to be an array, but is "${typeof value}"`, ); } - return value.map((value) => this.objectInputType.prepareValue(value, context)); + return value.map((value) => + this.objectInputType.prepareValue(value as PlainObject, context), + ); } collectAffectedFields(value: AnyValue, fields: Set, context: FieldContext) { @@ -238,14 +240,14 @@ export class UpdateValueObjectListInputField extends BasicUpdateInputField { if (value == undefined) { return; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value for "${this.name}" to be an array, but is "${typeof value}"`, ); } value.forEach((value) => - this.objectInputType.collectAffectedFields(value, fields, context), + this.objectInputType.collectAffectedFields(value as PlainObject, fields, context), ); } } diff --git a/src/schema-generation/update-input-types/relation-fields.ts b/src/schema-generation/update-input-types/relation-fields.ts index c1e95acc..de7747a3 100644 --- a/src/schema-generation/update-input-types/relation-fields.ts +++ b/src/schema-generation/update-input-types/relation-fields.ts @@ -6,7 +6,7 @@ import { getCreateRelatedEntityFieldName, getRemoveRelationFieldName, } from '../../schema/names'; -import { AnyValue, PlainObject } from '../../utils/utils'; +import { AnyValue, isReadonlyArray, PlainObject } from '../../utils/utils'; import { CreateRootEntityInputType } from '../create-input-types'; import { FieldContext } from '../query-node-object-type'; import { @@ -85,7 +85,7 @@ export class AddEdgesInputField extends AbstractRelationUpdateInputField { if (value == undefined) { return []; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value of "${this.name}" to be an array, but is ${typeof value}`, ); @@ -110,7 +110,7 @@ export class RemoveEdgesInputField extends AbstractRelationUpdateInputField { if (value == undefined) { return []; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value of "${this.name}" to be an array, but is ${typeof value}`, ); @@ -140,7 +140,7 @@ export class CreateAndAddEdgesInputField extends AbstractRelationUpdateInputFiel if (value == undefined) { return []; } - if (!Array.isArray(value)) { + if (!isReadonlyArray(value)) { throw new Error( `Expected value of "${this.name}" to be an array, but is ${typeof value}`, ); @@ -177,7 +177,7 @@ export class CreateAndSetEdgeInputField extends AbstractRelationUpdateInputField if (value == undefined) { return []; } - if (Array.isArray(value)) { + if (isReadonlyArray(value)) { throw new Error( `Expected value of "${this.name}" to be an object, but is ${typeof value}`, ); diff --git a/src/schema-generation/utils/map.ts b/src/schema-generation/utils/map.ts index c191cae8..989b5967 100644 --- a/src/schema-generation/utils/map.ts +++ b/src/schema-generation/utils/map.ts @@ -10,6 +10,7 @@ import { TransformListQueryNode, VariableQueryNode, } from '../../query-tree'; +import { isReadonlyArray } from '../../utils/utils'; export function getMapNode(listNode: QueryNode, projection: (itemNode: QueryNode) => QueryNode) { if (listNode instanceof ListQueryNode) { @@ -61,7 +62,7 @@ export function mapToIDNodesWithOptimizations(listNode: QueryNode): QueryNode { if ( filterNode.operator === BinaryOperator.IN && filterNode.rhs instanceof LiteralQueryNode && - Array.isArray(filterNode.rhs.value) + isReadonlyArray(filterNode.rhs.value) ) { if (listNode.maxCount == undefined) { return filterNode.rhs; diff --git a/src/schema/constants.ts b/src/schema/constants.ts index 314c878c..14ab325a 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -129,9 +129,9 @@ export const CALC_MUTATIONS_OPERATORS_ARG = 'operators'; export type CalcMutationOperator = { name: 'MULTIPLY' | 'DIVIDE' | 'ADD' | 'SUBTRACT' | 'MODULO' | 'APPEND' | 'PREPEND'; prefix: string; - supportedTypes: string[]; + supportedTypes: ReadonlyArray; }; -export const CALC_MUTATIONS_OPERATORS: CalcMutationOperator[] = [ +export const CALC_MUTATIONS_OPERATORS: ReadonlyArray = [ { name: 'MULTIPLY', prefix: 'multiplyWith_', supportedTypes: numberTypeNames }, { name: 'DIVIDE', prefix: 'divideBy_', supportedTypes: numberTypeNames }, { name: 'ADD', prefix: 'addTo_', supportedTypes: numberTypeNames }, diff --git a/src/schema/preparation/ast-validation-modules/indices-validator.ts b/src/schema/preparation/ast-validation-modules/indices-validator.ts index eee509eb..045d99cd 100644 --- a/src/schema/preparation/ast-validation-modules/indices-validator.ts +++ b/src/schema/preparation/ast-validation-modules/indices-validator.ts @@ -13,7 +13,7 @@ export const VALIDATION_ERROR_INDICES_ONLY_ON_ROOT_ENTITIES = 'Indices are only allowed in root entity fields. You can add indices to fields of embedded objects with @rootEntities(indices: [...]).'; export class IndicesValidator implements ASTValidator { - validate(ast: DocumentNode): ValidationMessage[] { + validate(ast: DocumentNode): ReadonlyArray { const validationMessages: ValidationMessage[] = []; [ ...getChildEntityTypes(ast), diff --git a/src/schema/preparation/ast-validation-modules/key-field-validator.ts b/src/schema/preparation/ast-validation-modules/key-field-validator.ts index ca459042..8e21a6c8 100644 --- a/src/schema/preparation/ast-validation-modules/key-field-validator.ts +++ b/src/schema/preparation/ast-validation-modules/key-field-validator.ts @@ -8,7 +8,7 @@ export const VALIDATION_ERROR_INVALID_OBJECT_TYPE = 'A @key field can only be declared on root entities.'; export class KeyFieldValidator implements ASTValidator { - validate(ast: DocumentNode): ValidationMessage[] { + validate(ast: DocumentNode): ReadonlyArray { const validationMessages: ValidationMessage[] = []; getObjectTypes(ast).forEach((objectTypeDefinition) => { let counter = 0; diff --git a/src/schema/preparation/ast-validation-modules/no-lists-of-lists-validator.ts b/src/schema/preparation/ast-validation-modules/no-lists-of-lists-validator.ts index 3e9749b4..477e1c3a 100644 --- a/src/schema/preparation/ast-validation-modules/no-lists-of-lists-validator.ts +++ b/src/schema/preparation/ast-validation-modules/no-lists-of-lists-validator.ts @@ -6,7 +6,7 @@ import { ASTValidator } from '../ast-validator'; export const VALIDATION_ERROR_LISTS_OF_LISTS_NOT_ALLOWED = 'Lists of lists are not allowed.'; export class NoListsOfListsValidator implements ASTValidator { - validate(ast: DocumentNode): ValidationMessage[] { + validate(ast: DocumentNode): ReadonlyArray { const validationMessages: ValidationMessage[] = []; getObjectTypes(ast).forEach((ot) => (ot.fields || []).forEach((field) => { diff --git a/src/schema/preparation/ast-validation-modules/no-unused-non-root-object-types-validator.ts b/src/schema/preparation/ast-validation-modules/no-unused-non-root-object-types-validator.ts index 358c906e..195b429f 100644 --- a/src/schema/preparation/ast-validation-modules/no-unused-non-root-object-types-validator.ts +++ b/src/schema/preparation/ast-validation-modules/no-unused-non-root-object-types-validator.ts @@ -9,7 +9,7 @@ import { import { ASTValidator } from '../ast-validator'; export class NoUnusedNonRootObjectTypesValidator implements ASTValidator { - validate(ast: DocumentNode): ValidationMessage[] { + validate(ast: DocumentNode): ReadonlyArray { // store all object types to a set const objectTypeNames = new Set( getObjectTypes(ast).map((objectType) => objectType), diff --git a/src/schema/preparation/ast-validation-modules/roles-on-non-root-entity-types.ts b/src/schema/preparation/ast-validation-modules/roles-on-non-root-entity-types.ts index dd1a41d9..a454ad84 100644 --- a/src/schema/preparation/ast-validation-modules/roles-on-non-root-entity-types.ts +++ b/src/schema/preparation/ast-validation-modules/roles-on-non-root-entity-types.ts @@ -8,7 +8,7 @@ export const VALIDATION_ERROR_ROLES_ON_NON_ROOT_ENTITY_TYPE = '@roles is only allowed on fields and on root entity types.'; export class RolesOnNonRootEntityTypesValidator implements ASTValidator { - validate(ast: DocumentNode): ValidationMessage[] { + validate(ast: DocumentNode): ReadonlyArray { const validationMessages: ValidationMessage[] = []; getObjectTypes(ast) .filter((obj) => !findDirectiveWithName(obj, ROOT_ENTITY_DIRECTIVE)) diff --git a/src/schema/preparation/source-validation-modules/graphql-rules.ts b/src/schema/preparation/source-validation-modules/graphql-rules.ts index cc9dab94..8365ff6d 100644 --- a/src/schema/preparation/source-validation-modules/graphql-rules.ts +++ b/src/schema/preparation/source-validation-modules/graphql-rules.ts @@ -40,7 +40,7 @@ const rules: ReadonlyArray = [ const sdlRules: ReadonlyArray = [UniqueEnumValueNamesRule]; export class GraphQLRulesValidator implements ParsedSourceValidator { - validate(source: ParsedProjectSource): ValidationMessage[] { + validate(source: ParsedProjectSource): ReadonlyArray { if (source.kind != ParsedProjectSourceBaseKind.GRAPHQL) { return []; } diff --git a/src/schema/preparation/source-validation-modules/permission-profile-validator.ts b/src/schema/preparation/source-validation-modules/permission-profile-validator.ts index a818e52c..4f145bac 100644 --- a/src/schema/preparation/source-validation-modules/permission-profile-validator.ts +++ b/src/schema/preparation/source-validation-modules/permission-profile-validator.ts @@ -8,7 +8,7 @@ import { createRoleSpecifierEntry, InvalidRoleSpecifierError, } from '../../../model/implementation/permission-profile'; -import { flatMap } from '../../../utils/utils'; +import { flatMap, isReadonlyArray } from '../../../utils/utils'; import { ParsedSourceValidator } from '../ast-validator'; export class PermissionProfileValidator implements ParsedSourceValidator { @@ -22,7 +22,7 @@ export class PermissionProfileValidator implements ParsedSourceValidator { if ( !data || !data.permissionProfiles || - Array.isArray(data.permissionProfiles) || + isReadonlyArray(data.permissionProfiles) || typeof data.permissionProfiles !== 'object' ) { return []; @@ -46,7 +46,7 @@ export class PermissionProfileValidator implements ParsedSourceValidator { name: string; pathLocationMap: PathLocationMap; }): ReadonlyArray { - if (!profile || !Array.isArray(profile.permissions)) { + if (!profile || !isReadonlyArray(profile.permissions)) { return []; } @@ -66,7 +66,7 @@ export class PermissionProfileValidator implements ParsedSourceValidator { index: string; pathLocationMap: PathLocationMap; }): ReadonlyArray { - if (!permission || !Array.isArray(permission.roles)) { + if (!permission || !isReadonlyArray(permission.roles)) { return []; } @@ -101,7 +101,7 @@ export class PermissionProfileValidator implements ParsedSourceValidator { if ( hasRoleExpressionWithoutCaptureGroup && - Array.isArray(permission.restrictToAccessGroups) + isReadonlyArray(permission.restrictToAccessGroups) ) { let accessGroupIndex = 0; for (const accessGroupExpression of permission.restrictToAccessGroups) { diff --git a/src/schema/preparation/source-validation-modules/sidecar-schema.ts b/src/schema/preparation/source-validation-modules/sidecar-schema.ts index 9e001b49..780cf4e1 100644 --- a/src/schema/preparation/source-validation-modules/sidecar-schema.ts +++ b/src/schema/preparation/source-validation-modules/sidecar-schema.ts @@ -4,7 +4,7 @@ import { ParsedSourceValidator } from '../ast-validator'; import validate from './schema/validate-schema'; export class SidecarSchemaValidator implements ParsedSourceValidator { - validate(source: ParsedProjectSource): ValidationMessage[] { + validate(source: ParsedProjectSource): ReadonlyArray { if (source.kind != ParsedProjectSourceBaseKind.OBJECT) { return []; } @@ -22,8 +22,7 @@ export class SidecarSchemaValidator implements ParsedSourceValidator { // we allow top-level additional properties because they indicate new features, so it might be ok to omit them const isWarning = - err.instancePath === '' && - err.message === 'must NOT have additional properties'; + err.instancePath === '' && err.message === 'must NOT have additional properties'; if (isWarning) { if (path in source.pathLocationMap) { const loc = source.pathLocationMap[path]; diff --git a/src/schema/preparation/transformation-pipeline.ts b/src/schema/preparation/transformation-pipeline.ts index 1a9f6864..24675397 100644 --- a/src/schema/preparation/transformation-pipeline.ts +++ b/src/schema/preparation/transformation-pipeline.ts @@ -4,7 +4,7 @@ import { ParsedProject, ParsedProjectSourceBaseKind } from '../../config/parsed- import { DatabaseAdapter } from '../../database/database-adapter'; import { AddNamespacesToTypesTransformer } from './pre-merge-ast-transformation-modules/add-namespaces-to-types-transformer'; -const preMergePipeline: ASTTransformer[] = [new AddNamespacesToTypesTransformer()]; +const preMergePipeline: ReadonlyArray = [new AddNamespacesToTypesTransformer()]; export function executePreMergeTransformationPipeline(parsedProject: ParsedProject): ParsedProject { return { diff --git a/src/schema/scalars/string-map.ts b/src/schema/scalars/string-map.ts index aa55c928..cae9bfcd 100644 --- a/src/schema/scalars/string-map.ts +++ b/src/schema/scalars/string-map.ts @@ -1,7 +1,8 @@ import { ASTNode, GraphQLScalarType, Kind } from 'graphql'; +import { isReadonlyArray } from '../../utils/utils'; function ensureStringMap(value: any) { - if (typeof value !== 'object' || value === null || Array.isArray(value)) { + if (typeof value !== 'object' || value === null || isReadonlyArray(value)) { throw new TypeError(`Expected object value`); } let hasClonedValue = false; diff --git a/src/schema/schema-builder.ts b/src/schema/schema-builder.ts index 13710fd0..7629cd19 100644 --- a/src/schema/schema-builder.ts +++ b/src/schema/schema-builder.ts @@ -40,7 +40,7 @@ import { MessageLocation } from '../model/'; import { Project, ProjectOptions } from '../project/project'; import { ProjectSource, SourceType } from '../project/source'; import { SchemaGenerator } from '../schema-generation'; -import { flatMap, PlainObject } from '../utils/utils'; +import { flatMap, isReadonlyArray, PlainObject } from '../utils/utils'; import { validateParsedProjectSource, validatePostMerge, @@ -285,7 +285,7 @@ function parseJSONSource( const data: PlainObject = parseResult.data; // arrays are not forbidden by json-lint - if (Array.isArray(data)) { + if (isReadonlyArray(data)) { validationContext.addMessage( ValidationMessage.error( `JSON file should define an object (is array)`, @@ -453,7 +453,7 @@ export function extractJSONFromYAML( return undefined; } - if (Array.isArray(result)) { + if (isReadonlyArray(result)) { validationContext.addMessage( ValidationMessage.error( `YAML file should define an object (is array)`, diff --git a/src/schema/schema-utils.ts b/src/schema/schema-utils.ts index 98aca04b..3e1bd9f5 100644 --- a/src/schema/schema-utils.ts +++ b/src/schema/schema-utils.ts @@ -27,113 +27,62 @@ import { CORE_SCALARS } from './graphql-base'; /** * Get all @link ObjectTypeDefinitionNode a model. * @param {DocumentNode} model (ast) - * @returns {ObjectTypeDefinitionNode[]} + * @returns {ReadonlyArray} */ export function getObjectTypes(model: DocumentNode): ReadonlyArray { - return ( - model.definitions.filter((def) => def.kind === Kind.OBJECT_TYPE_DEFINITION) - ); + return model.definitions.filter((def) => def.kind === Kind.OBJECT_TYPE_DEFINITION); } -export function getEnumTypes(model: DocumentNode): ReadonlyArray { - return ( - model.definitions.filter((def) => def.kind === Kind.ENUM_TYPE_DEFINITION) - ); +export function getEnumTypes(model: DocumentNode): ReadonlyArray { + return model.definitions.filter((def) => def.kind === Kind.ENUM_TYPE_DEFINITION); } /** * Get all @link ObjectTypeDefinitionNode annotated with @rootEntity directive of a model. - * @param {DocumentNode} model (ast) - * @returns {ObjectTypeDefinitionNode[]} */ export function getRootEntityTypes(model: DocumentNode): ReadonlyArray { - return ( - model.definitions.filter( - (def) => - def.kind === Kind.OBJECT_TYPE_DEFINITION && - def.directives && - def.directives.some((directive) => directive.name.value === ROOT_ENTITY_DIRECTIVE), - ) + return getObjectTypes(model).filter( + (def) => + def.directives && + def.directives.some((directive) => directive.name.value === ROOT_ENTITY_DIRECTIVE), ); } /** * Get all @link ObjectTypeDefinitionNode annotated with @childEntity directive of a model. - * @param {DocumentNode} model (ast) - * @returns {ObjectTypeDefinitionNode[]} */ export function getChildEntityTypes(model: DocumentNode): ReadonlyArray { - return ( - model.definitions.filter( - (def) => - def.kind === Kind.OBJECT_TYPE_DEFINITION && - def.directives && - def.directives.some((directive) => directive.name.value === CHILD_ENTITY_DIRECTIVE), - ) + return getObjectTypes(model).filter( + (def) => + def.directives && + def.directives.some((directive) => directive.name.value === CHILD_ENTITY_DIRECTIVE), ); } /** * Get all @link ObjectTypeDefinitionNode annotated with @entityExtension directive of a model. - * @param {DocumentNode} model (ast) - * @returns {ObjectTypeDefinitionNode[]} */ export function getEntityExtensionTypes( model: DocumentNode, ): ReadonlyArray { - return ( - model.definitions.filter( - (def) => - def.kind === Kind.OBJECT_TYPE_DEFINITION && - def.directives && - def.directives.some( - (directive) => directive.name.value === ENTITY_EXTENSION_DIRECTIVE, - ), - ) + return getObjectTypes(model).filter( + (def) => + def.directives && + def.directives.some((directive) => directive.name.value === ENTITY_EXTENSION_DIRECTIVE), ); } /** * Get all @link ObjectTypeDefinitionNode annotated with @valueObject directive of a model. - * @param {DocumentNode} model (ast) - * @returns {ObjectTypeDefinitionNode[]} */ export function getValueObjectTypes(model: DocumentNode): ReadonlyArray { - return ( - model.definitions.filter( - (def) => - def.kind === Kind.OBJECT_TYPE_DEFINITION && - def.directives && - def.directives.some((directive) => directive.name.value === VALUE_OBJECT_DIRECTIVE), - ) + return getObjectTypes(model).filter( + (def) => + def.directives && + def.directives.some((directive) => directive.name.value === VALUE_OBJECT_DIRECTIVE), ); } -function getScalarFieldsOfObjectDefinition( - ast: DocumentNode, - objectDefinition: ObjectTypeDefinitionNode, -): ReadonlyArray { - return (objectDefinition.fields || []).filter((field) => { - switch (field.type.kind) { - case Kind.NAMED_TYPE: - return ( - getNamedTypeDefinitionAST(ast, field.type.name.value).kind === - Kind.SCALAR_TYPE_DEFINITION - ); - case Kind.NON_NULL_TYPE: - if (field.type.type.kind !== Kind.NAMED_TYPE) { - return false; - } - return ( - getNamedTypeDefinitionAST(ast, field.type.type.name.value).kind === - Kind.SCALAR_TYPE_DEFINITION - ); - default: - return false; - } - }); -} - function getNamedTypeDefinitionASTIfExists( ast: DocumentNode, name: string, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 2ca9510b..bcf16d4a 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,11 +1,11 @@ export type PlainObject = { [key: string]: AnyValue }; export type AnyValue = unknown; -export type Constructor = { new (...args: any[]): T }; +export type Constructor = { new (...args: ReadonlyArray): T }; export function flatMap( arr: ReadonlyArray, f: (t: TIn) => ReadonlyArray, -): TOut[] { +): ReadonlyArray { return arr.reduce((ys: any, x: any) => { return ys.concat(f.call(null, x)); }, []); @@ -33,7 +33,7 @@ export function mapFirstDefined( return undefined; } -export function flatten(arr: ReadonlyArray>): T[] { +export function flatten(arr: ReadonlyArray>): ReadonlyArray { return arr.reduce((ys: any, x: any) => { return ys.concat(x); }, []); @@ -68,12 +68,12 @@ export function decapitalize(string: string) { * Groups items in an array by common keys * @param items the input items * @param keyFn a function that computes the key value of an item - * @returns {Map} a map from key values to the list of items that have that key + * @returns {Map>} a map from key values to the list of items that have that key */ export function groupArray( items: ReadonlyArray, keyFn: (item: TItem) => TKey, -): Map { +): Map> { const map = new Map(); for (const item of items) { const key = keyFn(item); @@ -114,7 +114,7 @@ export function indent(input: string, indentation: string | number = INDENTATION * @param count the number of items for the array * @returns the array */ -export function range(count: number): number[] { +export function range(count: number): ReadonlyArray { return Array.from(Array(count).keys()); } @@ -138,11 +138,11 @@ export function arrayToObject( return result; } -export function compact(arr: ReadonlyArray): T[] { - return arr.filter((a) => a != undefined) as T[]; +export function compact(arr: ReadonlyArray): ReadonlyArray { + return arr.filter((a) => a != undefined) as ReadonlyArray; } -export function objectValues(obj: { [name: string]: T }): T[] { +export function objectValues(obj: { [name: string]: T }): ReadonlyArray { return Object.keys(obj).map((i) => obj[i]); } @@ -281,3 +281,14 @@ export function joinWithAnd(items: ReadonlyArray): string { const last = upToSecondLast.pop(); return upToSecondLast.join(', ') + ', and ' + last; } + +/** + * Checks if a value is an array using Array.isArray() + * + * The runtime behavior is identical to Array.is.Array(), but it fixes a shortcoming of TypeScript if the + * passed value is a union of a readonly array and something else. Array.isArray() would narrow the value + * to Array instead of ReadonlyArray (see https://github.com/microsoft/TypeScript/issues/17002). + */ +export function isReadonlyArray(obj: unknown | ReadonlyArray): obj is ReadonlyArray { + return Array.isArray(obj); +} diff --git a/src/utils/visitor.ts b/src/utils/visitor.ts index 41d852f3..f2f4c403 100644 --- a/src/utils/visitor.ts +++ b/src/utils/visitor.ts @@ -1,3 +1,5 @@ +import { isReadonlyArray } from './utils'; + export type VisitResult = { recurse?: boolean; newValue: T; @@ -43,14 +45,14 @@ function visitObjectProperties(object: T, visitor: Visitor) } function visitObjectOrArray( - nodeOrArray: T | T[], + nodeOrArray: T | ReadonlyArray, visitor: Visitor, key: string | undefined, -): T | T[] { +): T | ReadonlyArray { if (typeof nodeOrArray != 'object' || nodeOrArray === null) { return nodeOrArray; } - if (!Array.isArray(nodeOrArray)) { + if (!isReadonlyArray(nodeOrArray)) { return visitObject0(nodeOrArray as T, visitor, key); } return nodeOrArray.map((item) => visitObject0(item, visitor, key));