diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index ed62137dd..ec580c68d 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: specifier: 3.7.13 version: 3.7.13(graphql@16.10.0)(react@17.0.2)(subscriptions-transport-ws@0.11.0(graphql@16.10.0)) '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -47,7 +47,7 @@ importers: version: 8.18.0 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/jsonwebtoken': specifier: 9.0.8 @@ -104,10 +104,10 @@ importers: ../../packages/cli: dependencies: '@boostercloud/framework-core': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-core '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -150,10 +150,10 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/application-tester': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../application-tester '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@oclif/test': specifier: ^4.1.10 @@ -264,7 +264,7 @@ importers: ../../packages/framework-common-helpers: dependencies: '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -280,7 +280,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -370,10 +370,10 @@ importers: ../../packages/framework-core: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-common-helpers '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect/cli': specifier: 0.56.2 @@ -437,10 +437,10 @@ importers: version: 8.18.0 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@boostercloud/metadata-booster': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../metadata-booster '@types/chai': specifier: 4.2.18 @@ -545,22 +545,22 @@ importers: ../../packages/framework-integration-tests: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-common-helpers '@boostercloud/framework-core': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-core '@boostercloud/framework-provider-aws': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-aws '@boostercloud/framework-provider-azure': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-azure '@boostercloud/framework-provider-local': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-local '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -618,25 +618,25 @@ importers: specifier: 3.7.13 version: 3.7.13(graphql@16.10.0)(react@17.0.2)(subscriptions-transport-ws@0.11.0(graphql@16.10.0)) '@boostercloud/application-tester': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../application-tester '@boostercloud/cli': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../cli '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@boostercloud/framework-provider-aws-infrastructure': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-aws-infrastructure '@boostercloud/framework-provider-azure-infrastructure': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-azure-infrastructure '@boostercloud/framework-provider-local-infrastructure': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-local-infrastructure '@boostercloud/metadata-booster': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../metadata-booster '@seald-io/nedb': specifier: 4.0.2 @@ -777,10 +777,10 @@ importers: ../../packages/framework-provider-aws: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-common-helpers '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -790,7 +790,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/aws-lambda': specifier: 8.10.48 @@ -943,13 +943,13 @@ importers: specifier: ^1.170.0 version: 1.204.0 '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-common-helpers '@boostercloud/framework-provider-aws': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-aws '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -983,7 +983,7 @@ importers: version: 1.10.2 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/archiver': specifier: 5.1.0 @@ -1097,10 +1097,10 @@ importers: specifier: ~1.1.0 version: 1.1.3 '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-common-helpers '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -1110,7 +1110,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -1203,16 +1203,16 @@ importers: specifier: ~4.7.0 version: 4.7.0 '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-common-helpers '@boostercloud/framework-core': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-core '@boostercloud/framework-provider-azure': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-azure '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@cdktf/provider-azurerm': specifier: 13.18.0 @@ -1279,7 +1279,7 @@ importers: version: 11.0.5 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -1360,10 +1360,10 @@ importers: ../../packages/framework-provider-local: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-common-helpers '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -1379,7 +1379,7 @@ importers: version: 8.18.0 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -1475,13 +1475,13 @@ importers: ../../packages/framework-provider-local-infrastructure: dependencies: '@boostercloud/framework-common-helpers': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-common-helpers '@boostercloud/framework-provider-local': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-provider-local '@boostercloud/framework-types': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../framework-types '@effect-ts/core': specifier: ^0.60.4 @@ -1500,7 +1500,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/chai': specifier: 4.2.18 @@ -1636,10 +1636,10 @@ importers: version: 8.18.0 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@boostercloud/metadata-booster': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../metadata-booster '@types/chai': specifier: 4.2.18 @@ -1733,7 +1733,7 @@ importers: version: 2.8.1 devDependencies: '@boostercloud/eslint-config': - specifier: workspace:^3.4.1 + specifier: workspace:^3.4.3 version: link:../../tools/eslint-config '@types/node': specifier: ^20.17.17 @@ -7395,8 +7395,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@6.0.0-dev.20250822: - resolution: {integrity: sha512-omHezTVn6vg+B/eFHkIzUGFvlbbkJdsdmdBohcsw8NMLyKOhKRMinE9aLu8f0EALT4R2YS41xak2KinK74/6Xg==} + typescript@6.0.0-dev.20251224: + resolution: {integrity: sha512-sDCiM/djY2WWuQnJFWH+AHnFN21s/5eHn8xeT2pBtvL8nfywCvUBW8EIjy8YTjniGpYqTfkU9xzs/q5qettJ/A==} engines: {node: '>=14.17'} hasBin: true @@ -10912,7 +10912,7 @@ snapshots: dependencies: semver: 7.6.3 shelljs: 0.8.5 - typescript: 6.0.0-dev.20250822 + typescript: 6.0.0-dev.20251224 dunder-proto@1.0.1: dependencies: @@ -14375,7 +14375,7 @@ snapshots: typescript@5.7.3: {} - typescript@6.0.0-dev.20250822: {} + typescript@6.0.0-dev.20251224: {} unbox-primitive@1.1.0: dependencies: diff --git a/packages/framework-common-helpers/src/http-service.ts b/packages/framework-common-helpers/src/http-service.ts index 5f8b88c3f..ab0ab88bc 100644 --- a/packages/framework-common-helpers/src/http-service.ts +++ b/packages/framework-common-helpers/src/http-service.ts @@ -1,7 +1,7 @@ import * as https from 'https' import { RequestOptions } from 'https' import * as http from 'http' -import { IncomingMessage } from 'node:http' +import { IncomingMessage, OutgoingHttpHeaders } from 'node:http' export interface PostConfiguration { contentType?: string @@ -29,7 +29,7 @@ export async function request( timeout: timeout, } if (data) { - options.headers!['Content-Length'] = data.length + ;(options.headers as OutgoingHttpHeaders)['Content-Length'] = data.length } return new Promise((resolve, reject) => { diff --git a/packages/framework-integration-tests/integration/provider-unaware/end-to-end/read-models.integration.ts b/packages/framework-integration-tests/integration/provider-unaware/end-to-end/read-models.integration.ts index a383469ae..a28330a4b 100644 --- a/packages/framework-integration-tests/integration/provider-unaware/end-to-end/read-models.integration.ts +++ b/packages/framework-integration-tests/integration/provider-unaware/end-to-end/read-models.integration.ts @@ -1550,10 +1550,13 @@ describe('Read models end-to-end tests', () => { if (process.env.TESTED_PROVIDER === 'AZURE' || process.env.TESTED_PROVIDER === 'LOCAL') { // Cursor can be either continuation token format or legacy offset format if (cursor.continuationToken) { - // New continuation token format + // Continuation token format - includes both continuationToken and page-based id expect(cursor.continuationToken).to.be.a('string') expect(cursor.continuationToken).to.not.be.empty - expect(cursor.id).to.be.undefined + expect(cursor.id).to.be.a('string') + expect(cursor.id).to.not.be.empty + // Verify page-based id matches the expected sequence (previousOffset + limit) + expect(cursor.id).to.equal((i + 1).toString()) } else if (cursor.id) { expect(cursor.id).to.be.a('string') expect(cursor.id).to.not.be.empty @@ -1617,11 +1620,11 @@ describe('Read models end-to-end tests', () => { expect(result.items.length).to.equal(1) expect(result.count).to.equal(1) - // Verify the returned cursor is correctly calculated using our fixed logic + // Verify the returned cursor is correctly calculated: previousOffset + limit if (result.cursor) { expect(result.cursor.id).to.be.a('string') expect(result.cursor.id).to.not.be.empty - // Should be '2' (1 + 1 result returned) based on our fixed logic: currentOffset + finalResources.length + // Should be '2' (offset 1 + limit 1) based on page-based cursor logic expect(result.cursor.id).to.equal('2') // Legacy cursors don't have continuation tokens expect(result.cursor.continuationToken).to.be.undefined diff --git a/packages/framework-provider-azure/src/helpers/query-helper.ts b/packages/framework-provider-azure/src/helpers/query-helper.ts index 21d1a9240..538d33990 100644 --- a/packages/framework-provider-azure/src/helpers/query-helper.ts +++ b/packages/framework-provider-azure/src/helpers/query-helper.ts @@ -67,26 +67,57 @@ export async function search( const canUseContinuationToken = !isDistinctQuery const hasLegacyCursor = typeof afterCursor?.id === 'string' && /^\d+$/.test(afterCursor.id) + logger.debug('PAGINATION DEBUG - Initial analysis:', { + isDistinctQuery, + canUseContinuationToken, + hasLegacyCursor, + afterCursor, + limit, + finalQuery, + }) + // Use Cosmos DB's continuation token pagination const feedOptions: FeedOptions = {} - if (limit) { - feedOptions.maxItemCount = limit - } + // Extract continuation token from the cursor (backward compatibility) if (afterCursor?.continuationToken) { feedOptions.continuationToken = afterCursor.continuationToken - } else if (!canUseContinuationToken || hasLegacyCursor) { + logger.debug('PAGINATION DEBUG - Using provided continuation token:', { + continuationToken: afterCursor.continuationToken, + feedOptions, + }) + } + + // Azure Cosmos DB requires maxItemCount when using continuation tokens + // Always set maxItemCount when limit is provided or when using continuation token + if (limit || afterCursor?.continuationToken) { + feedOptions.maxItemCount = limit ?? 100 + } + + if (!afterCursor?.continuationToken && (!canUseContinuationToken || hasLegacyCursor)) { // Legacy cursor format - fallback to OFFSET for backward compatibility const offset = afterCursor?.id ? parseInt(afterCursor.id) : 0 - let legacyQuery = `${finalQuery} OFFSET ${offset}` - if (limit) { - legacyQuery += ` LIMIT ${limit} ` - } + // Azure Cosmos DB requires LIMIT when using OFFSET + const effectiveLimit = limit ?? 100 + const legacyQuery = `${finalQuery} OFFSET ${offset} LIMIT ${effectiveLimit} ` const legacyQuerySpec = { ...querySpec, query: legacyQuery } + logger.debug('PAGINATION DEBUG - Using LEGACY OFFSET/LIMIT approach:', { + reason: !canUseContinuationToken ? 'DISTINCT query detected' : 'Legacy numeric cursor detected', + offset, + effectiveLimit, + legacyQuery, + legacyQuerySpec, + }) const { resources } = await container.items.query(legacyQuerySpec).fetchAll() const processedResources = processResources(resources) + logger.debug('PAGINATION DEBUG - Legacy query results:', { + resourcesCount: resources?.length || 0, + processedResourcesCount: processedResources.length, + nextCursor: { id: (offset + processedResources.length).toString() }, + }) + return { items: processedResources ?? [], count: processedResources.length, @@ -96,19 +127,50 @@ export async function search( } } + logger.debug('PAGINATION DEBUG - About to execute continuation token query with feedOptions:', feedOptions) const queryIterator = container.items.query(querySpec, feedOptions) const { resources, continuationToken } = await queryIterator.fetchNext() + logger.debug('PAGINATION DEBUG - Cosmos SDK response:', { + resourcesCount: resources?.length || 0, + continuationTokenReceived: !!continuationToken, + continuationTokenValue: continuationToken, + continuationTokenType: typeof continuationToken, + continuationTokenLength: continuationToken?.length, + }) + const finalResources = processResources(resources || []) + // cursor.id advances by the page size (limit) to maintain consistent page-based offsets + // that frontends rely on (e.g., limit=5 produces cursors 5, 10, 15, ...) + const previousOffset = afterCursor?.id ? parseInt(afterCursor.id) : 0 + const effectiveLimit = limit ?? 100 + let cursor: Record | undefined if (continuationToken) { - cursor = { continuationToken } + cursor = { continuationToken, id: (previousOffset + effectiveLimit).toString() } + logger.debug('PAGINATION DEBUG - Setting cursor with continuation token:', { + cursor, + previousOffset, + effectiveLimit, + }) } else if (finalResources.length > 0) { - const currentOffset = afterCursor?.id && !isNaN(parseInt(afterCursor.id)) ? parseInt(afterCursor.id) : 0 - cursor = { id: (currentOffset + finalResources.length).toString() } // Use the length of the results to calculate the next id + cursor = { id: (previousOffset + effectiveLimit).toString() } + logger.debug('PAGINATION DEBUG - No continuation token, setting fallback cursor:', { + cursor, + previousOffset, + effectiveLimit, + }) + } else { + logger.debug('PAGINATION DEBUG - No continuation token and no resources, no cursor set') } + logger.debug('PAGINATION DEBUG - Final response:', { + itemsCount: finalResources.length, + cursor, + hasMoreResults: !!continuationToken, + }) + return { items: finalResources, count: finalResources.length,