Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from './BasePostgresEntityDatabaseAdapter';
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder';
import { SQLFragment } from './SQLOperator';
import type { Connection, PageInfo } from './internal/EntityKnexDataManager';
import { EntityKnexDataManager } from './internal/EntityKnexDataManager';

export interface EntityLoaderOrderByClause<
Expand Down Expand Up @@ -75,6 +76,70 @@ export interface EntityLoaderQuerySelectionModifiersWithOrderByFragment<
orderByFragment?: SQLFragment;
}

/**
* Base pagination arguments
*/
interface EntityLoaderBasePaginationArgs<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields,
> {
/**
* SQLFragment representing the WHERE clause to filter the entities being paginated.
*/
where?: SQLFragment;

/**
* Order the entities by specified columns and orders. If the ID field is not included in the orderBy, it will be automatically included as the last orderBy field to ensure stable pagination.
*/
orderBy?: EntityLoaderOrderByClause<TFields, TSelectedFields>[];
}

/**
* Forward pagination arguments
*/
export interface EntityLoaderForwardPaginationArgs<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields,
> extends EntityLoaderBasePaginationArgs<TFields, TSelectedFields> {
/**
* The number of entities to return starting from the entity after the cursor (for forward pagination). Must be a positive integer.
*/
first: number;

/**
* The cursor to paginate after for forward pagination, typically an opaque string encoding of the values of the cursor fields of the last entity in the previous page. If not provided, pagination starts from the beginning of the result set.
*/
after?: string;
}

/**
* Backward pagination arguments
*/
export interface EntityLoaderBackwardPaginationArgs<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields,
> extends EntityLoaderBasePaginationArgs<TFields, TSelectedFields> {
/**
* The number of entities to return starting from the entity before the cursor (for backward pagination). Must be a positive integer.
*/
last: number;

/**
* The cursor to paginate before for backward pagination, typically an opaque string encoding of the values of the cursor fields of the first entity in the previous page. If not provided, pagination starts from the end of the result set.
*/
before?: string;
}

/**
* Load page pagination arguments, which can be either forward or backward pagination arguments.
*/
export type EntityLoaderLoadPageArgs<
TFields extends Record<string, any>,
TSelectedFields extends keyof TFields,
> =
| EntityLoaderForwardPaginationArgs<TFields, TSelectedFields>
| EntityLoaderBackwardPaginationArgs<TFields, TSelectedFields>;

/**
* Authorization-result-based knex entity loader for non-data-loader-based load methods.
* All loads through this loader are results (or null for some loader methods), where an
Expand Down Expand Up @@ -200,6 +265,47 @@ export class AuthorizationResultBasedKnexEntityLoader<
modifiers,
);
}

/**
* Load a page of entities with Relay-style cursor pagination.
* Only returns successfully authorized entities for cursor stability; failed authorization results are filtered out.
*
* @returns Connection with only successfully authorized entities
*/
async loadPageBySQLAsync(
args: EntityLoaderLoadPageArgs<TFields, TSelectedFields>,
): Promise<Connection<TEntity>> {
const pageResult = await this.knexDataManager.loadPageBySQLFragmentAsync(
this.queryContext,
args,
);

const edgeResults = await Promise.all(
pageResult.edges.map(async (edge) => {
const entityResult = await this.constructionUtils.constructAndAuthorizeEntityAsync(
edge.node,
);
if (!entityResult.ok) {
return null;
}
return {
...edge,
node: entityResult.value,
};
}),
);
const edges = edgeResults.filter((edge) => edge !== null);
const pageInfo: PageInfo = {
...pageResult.pageInfo,
startCursor: edges[0]?.cursor ?? null,
endCursor: edges[edges.length - 1]?.cursor ?? null,
};

return {
edges,
pageInfo,
};
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { EntityPrivacyPolicy, ReadonlyEntity, ViewerContext } from '@expo/entity';
import {
EntityConstructionUtils,
EntityPrivacyPolicy,
EntityQueryContext,
IEntityMetricsAdapter,
ReadonlyEntity,
ViewerContext,
} from '@expo/entity';

import {
AuthorizationResultBasedKnexEntityLoader,
EntityLoaderLoadPageArgs,
EntityLoaderQuerySelectionModifiers,
EntityLoaderQuerySelectionModifiersWithOrderByFragment,
EntityLoaderQuerySelectionModifiersWithOrderByRaw,
} from './AuthorizationResultBasedKnexEntityLoader';
import { FieldEqualityCondition } from './BasePostgresEntityDatabaseAdapter';
import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder';
import { SQLFragment } from './SQLOperator';
import type { Connection, EntityKnexDataManager } from './internal/EntityKnexDataManager';

/**
* Enforcing knex entity loader for non-data-loader-based load methods.
Expand Down Expand Up @@ -37,6 +46,17 @@ export class EnforcingKnexEntityLoader<
TPrivacyPolicy,
TSelectedFields
>,
private readonly queryContext: EntityQueryContext,
private readonly knexDataManager: EntityKnexDataManager<TFields, TIDField>,
protected readonly metricsAdapter: IEntityMetricsAdapter,
private readonly constructionUtils: EntityConstructionUtils<
TFields,
TIDField,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
) {}

/**
Expand Down Expand Up @@ -167,6 +187,59 @@ export class EnforcingKnexEntityLoader<
> {
return new EnforcingSQLQueryBuilder(this.knexEntityLoader, fragment, modifiers);
}

/**
* Load a page of entities with Relay-style cursor pagination.
*
* @param args - Pagination arguments with either first/after or last/before
*
* @example
* ```typescript
* // Forward pagination - get first 10 items
* const users = await TestEntity.knexLoader(vc)
* .loadPageBySQLAsync({
* first: 10,
* where: sql`age > ${18}`,
* orderBy: 'created_at'
* });
*
* // Backward pagination with cursor - get last 10 items before the cursor
* const lastResults = await TestEntity.knexLoader(vc)
* .loadPageBySQLAsync({
* last: 10,
* where: sql`status = ${'active'}`,
* before: cursor,
* });
* ```
*
* @throws EntityNotAuthorizedError if viewer is not authorized to view any returned entity
*/
async loadPageBySQLAsync(
args: EntityLoaderLoadPageArgs<TFields, TSelectedFields>,
): Promise<Connection<TEntity>> {
const pageResult = await this.knexDataManager.loadPageBySQLFragmentAsync(
this.queryContext,
args,
);

const edges = await Promise.all(
pageResult.edges.map(async (edge) => {
const entityResult = await this.constructionUtils.constructAndAuthorizeEntityAsync(
edge.node,
);
const entity = entityResult.enforceValue();
return {
...edge,
node: entity,
};
}),
);

return {
edges,
pageInfo: pageResult.pageInfo,
};
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export class KnexEntityLoader<
TPrivacyPolicy,
TSelectedFields
> {
return new EnforcingKnexEntityLoader(this.withAuthorizationResults());
return this.viewerContext
.getViewerScopedEntityCompanionForClass(this.entityClass)
.getKnexLoaderFactory()
.forLoadEnforcing(this.queryContext, { previousValue: null, cascadingDeleteCause: null });
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { EntityConstructionUtils } from '@expo/entity/src/EntityConstructionUtils';

import { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader';
import { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader';
import { EntityKnexDataManager } from './internal/EntityKnexDataManager';

/**
Expand Down Expand Up @@ -83,4 +84,47 @@ export class KnexEntityLoaderFactory<
constructionUtils,
);
}

/**
* Vend enforcing knex loader for loading an entity in a given query context.
* @param viewerContext - viewer context of loading user
* @param queryContext - query context in which to perform the load
*/
forLoadEnforcing(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext<
TFields,
TIDField,
TViewerContext,
TEntity,
TSelectedFields
>,
): EnforcingKnexEntityLoader<
TFields,
TIDField,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
> {
const constructionUtils = new EntityConstructionUtils(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
this.entityCompanion.entityCompanionDefinition.entityConfiguration,
this.entityCompanion.entityCompanionDefinition.entityClass,
this.entityCompanion.entityCompanionDefinition.entitySelectedFields,
this.entityCompanion.privacyPolicy,
this.metricsAdapter,
);

return new EnforcingKnexEntityLoader(
this.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext),
queryContext,
this.knexDataManager,
this.metricsAdapter,
constructionUtils,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@expo/entity';

import { AuthorizationResultBasedKnexEntityLoader } from './AuthorizationResultBasedKnexEntityLoader';
import { EnforcingKnexEntityLoader } from './EnforcingKnexEntityLoader';
import { KnexEntityLoaderFactory } from './KnexEntityLoaderFactory';

/**
Expand Down Expand Up @@ -61,4 +62,28 @@ export class ViewerScopedKnexEntityLoaderFactory<
privacyPolicyEvaluationContext,
);
}

forLoadEnforcing(
queryContext: EntityQueryContext,
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext<
TFields,
TIDField,
TViewerContext,
TEntity,
TSelectedFields
>,
): EnforcingKnexEntityLoader<
TFields,
TIDField,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
> {
return this.knexEntityLoaderFactory.forLoadEnforcing(
this.viewerContext,
queryContext,
privacyPolicyEvaluationContext,
);
}
}
Loading