chore: resolve agent comments
This commit is contained in:
@@ -38,12 +38,13 @@ export const handleListEntitiesInternal = createRoute({
|
||||
eq(entities.deleted, false),
|
||||
);
|
||||
|
||||
const searchCondition = search
|
||||
const escapedSearch = search?.replace(/[%_\\]/g, "\\$&");
|
||||
const searchCondition = escapedSearch
|
||||
? and(
|
||||
baseConditions,
|
||||
or(
|
||||
ilike(entities.id, `%${search}%`),
|
||||
ilike(entities.name, `%${search}%`),
|
||||
ilike(entities.id, `%${escapedSearch}%`),
|
||||
ilike(entities.name, `%${escapedSearch}%`),
|
||||
),
|
||||
)
|
||||
: baseConditions;
|
||||
@@ -63,7 +64,6 @@ export const handleListEntitiesInternal = createRoute({
|
||||
|
||||
return c.json({
|
||||
list: results as Entity[],
|
||||
total: results.length,
|
||||
total_count: countResult[0]?.count ?? 0,
|
||||
});
|
||||
},
|
||||
|
||||
39
server/tests/unit/escapeIlikePattern.test.ts
Normal file
39
server/tests/unit/escapeIlikePattern.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
const escapeIlikePattern = (input: string): string =>
|
||||
input.replace(/[%_\\]/g, "\\$&");
|
||||
|
||||
describe("escapeIlikePattern", () => {
|
||||
test("passes through plain text unchanged", () => {
|
||||
expect(escapeIlikePattern("hello")).toBe("hello");
|
||||
});
|
||||
|
||||
test("escapes % wildcard", () => {
|
||||
expect(escapeIlikePattern("50%")).toBe("50\\%");
|
||||
});
|
||||
|
||||
test("escapes _ single-char wildcard", () => {
|
||||
expect(escapeIlikePattern("us_r")).toBe("us\\_r");
|
||||
});
|
||||
|
||||
test("escapes backslash", () => {
|
||||
expect(escapeIlikePattern("path\\to")).toBe("path\\\\to");
|
||||
});
|
||||
|
||||
test("escapes multiple wildcards in one string", () => {
|
||||
expect(escapeIlikePattern("a%b_c\\d")).toBe("a\\%b\\_c\\\\d");
|
||||
});
|
||||
|
||||
test("handles empty string", () => {
|
||||
expect(escapeIlikePattern("")).toBe("");
|
||||
});
|
||||
|
||||
test("leaves normal entity IDs unchanged", () => {
|
||||
expect(escapeIlikePattern("dispersive-preview")).toBe(
|
||||
"dispersive-preview",
|
||||
);
|
||||
expect(escapeIlikePattern("6a13cf0b9b6845d6edbb4aa4")).toBe(
|
||||
"6a13cf0b9b6845d6edbb4aa4",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -47,7 +47,8 @@ export const useCusQuery = ({
|
||||
const entityAlreadyLoaded =
|
||||
!entityId ||
|
||||
(currentCustomer as FullCustomer)?.customer_products?.some(
|
||||
(cp: FullCusProduct) => cp.entity_id === entityId,
|
||||
(cp: FullCusProduct) =>
|
||||
cp.entity_id === entityId || cp.internal_entity_id === entityId,
|
||||
);
|
||||
|
||||
const effectiveEntityId = entityAlreadyLoaded ? null : entityId;
|
||||
|
||||
@@ -21,7 +21,6 @@ export const useEntitiesQuery = ({
|
||||
|
||||
const fetcher = async (): Promise<{
|
||||
list: Entity[];
|
||||
total: number;
|
||||
total_count: number;
|
||||
}> => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -42,7 +41,6 @@ export const useEntitiesQuery = ({
|
||||
|
||||
return {
|
||||
entities: (data?.list ?? []) as Entity[],
|
||||
total: data?.total ?? 0,
|
||||
totalCount: data?.total_count ?? 0,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
@@ -65,12 +65,14 @@ export const useEntitySelector = () => {
|
||||
enabled: isVisible,
|
||||
});
|
||||
|
||||
const selectedEntity = entities.find(
|
||||
(e) => e.id === entityId || e.internal_id === entityId,
|
||||
);
|
||||
const selectedEntity =
|
||||
entities.find((e) => e.id === entityId || e.internal_id === entityId) ??
|
||||
customerEntities.find((e) => e.id === entityId || e.internal_id === entityId);
|
||||
|
||||
const effectiveTotalCount = totalCount ?? customerEntities.length;
|
||||
|
||||
const entityTypeText = deriveEntityTypeText({
|
||||
totalCount: totalCount || customerEntities.length,
|
||||
totalCount: effectiveTotalCount,
|
||||
entities: entities.length > 0 ? entities : customerEntities,
|
||||
features: features ?? [],
|
||||
});
|
||||
@@ -79,7 +81,7 @@ export const useEntitySelector = () => {
|
||||
entities,
|
||||
selectedEntity,
|
||||
entityId,
|
||||
totalCount: totalCount || customerEntities.length,
|
||||
totalCount: effectiveTotalCount,
|
||||
entityTypeText,
|
||||
isLoading,
|
||||
isVisible,
|
||||
|
||||
@@ -26,7 +26,7 @@ function filterBySelectedEntity({
|
||||
|
||||
return products.filter(
|
||||
(product) =>
|
||||
!product.internal_entity_id ||
|
||||
(!product.internal_entity_id && !product.entity_id) ||
|
||||
product.internal_entity_id === selectedEntity.internal_id ||
|
||||
product.entity_id === selectedEntity.id,
|
||||
);
|
||||
@@ -41,8 +41,9 @@ export function useCustomerProductsData() {
|
||||
);
|
||||
|
||||
const { subscriptions, purchases, hasEntityProducts } = useMemo(() => {
|
||||
const allProducts = filterCustomerProducts({ customer, showExpired: showExpired ?? false });
|
||||
const filtered = filterBySelectedEntity({
|
||||
products: filterCustomerProducts({ customer, showExpired: showExpired ?? false }),
|
||||
products: allProducts,
|
||||
entityId,
|
||||
entities: customer.entities,
|
||||
});
|
||||
@@ -50,7 +51,7 @@ export function useCustomerProductsData() {
|
||||
return {
|
||||
subscriptions: filtered.filter((p) => !isOneOffCusProduct(p)),
|
||||
purchases: filtered.filter((p) => isOneOffCusProduct(p)),
|
||||
hasEntityProducts: filtered.some(
|
||||
hasEntityProducts: allProducts.some(
|
||||
(p) => p.internal_entity_id || p.entity_id,
|
||||
),
|
||||
};
|
||||
|
||||
131
vite/tests/views/customers2/hooks/filterBySelectedEntity.test.ts
Normal file
131
vite/tests/views/customers2/hooks/filterBySelectedEntity.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { Entity, FullCusProduct } from "@autumn/shared";
|
||||
|
||||
const makeProduct = (overrides: Partial<FullCusProduct> = {}): FullCusProduct =>
|
||||
({
|
||||
id: "cp-1",
|
||||
entity_id: null,
|
||||
internal_entity_id: null,
|
||||
...overrides,
|
||||
}) as unknown as FullCusProduct;
|
||||
|
||||
const makeEntity = (overrides: Partial<Entity> = {}): Entity =>
|
||||
({
|
||||
id: "ent-1",
|
||||
internal_id: "int-ent-1",
|
||||
...overrides,
|
||||
}) as unknown as Entity;
|
||||
|
||||
function filterBySelectedEntity({
|
||||
products,
|
||||
entityId,
|
||||
entities,
|
||||
}: {
|
||||
products: FullCusProduct[];
|
||||
entityId: string | null;
|
||||
entities: Entity[];
|
||||
}): FullCusProduct[] {
|
||||
if (!entityId) return products;
|
||||
|
||||
const selectedEntity = entities.find(
|
||||
(e: Entity) => e.id === entityId || e.internal_id === entityId,
|
||||
);
|
||||
if (!selectedEntity) return products;
|
||||
|
||||
return products.filter(
|
||||
(product) =>
|
||||
(!product.internal_entity_id && !product.entity_id) ||
|
||||
product.internal_entity_id === selectedEntity.internal_id ||
|
||||
product.entity_id === selectedEntity.id,
|
||||
);
|
||||
}
|
||||
|
||||
describe("filterBySelectedEntity", () => {
|
||||
const entity = makeEntity({ id: "ent-1", internal_id: "int-ent-1" });
|
||||
const otherEntity = makeEntity({ id: "ent-2", internal_id: "int-ent-2" });
|
||||
const entities = [entity, otherEntity];
|
||||
|
||||
test("returns all products when no entityId", () => {
|
||||
const products = [
|
||||
makeProduct({ id: "cp-1" }),
|
||||
makeProduct({ id: "cp-2", entity_id: "ent-1" }),
|
||||
];
|
||||
const result = filterBySelectedEntity({
|
||||
products,
|
||||
entityId: null,
|
||||
entities,
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("includes customer-level products (no entity)", () => {
|
||||
const customerProduct = makeProduct({ id: "cp-cus" });
|
||||
const entityProduct = makeProduct({
|
||||
id: "cp-ent",
|
||||
entity_id: "ent-1",
|
||||
internal_entity_id: "int-ent-1",
|
||||
});
|
||||
const result = filterBySelectedEntity({
|
||||
products: [customerProduct, entityProduct],
|
||||
entityId: "ent-1",
|
||||
entities,
|
||||
});
|
||||
expect(result).toContainEqual(customerProduct);
|
||||
expect(result).toContainEqual(entityProduct);
|
||||
});
|
||||
|
||||
test("excludes products from other entities", () => {
|
||||
const otherEntityProduct = makeProduct({
|
||||
id: "cp-other",
|
||||
entity_id: "ent-2",
|
||||
internal_entity_id: "int-ent-2",
|
||||
});
|
||||
const result = filterBySelectedEntity({
|
||||
products: [otherEntityProduct],
|
||||
entityId: "ent-1",
|
||||
entities,
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("does not leak products with only internal_entity_id set", () => {
|
||||
const leakyProduct = makeProduct({
|
||||
id: "cp-leak",
|
||||
entity_id: null,
|
||||
internal_entity_id: "int-ent-2",
|
||||
});
|
||||
const result = filterBySelectedEntity({
|
||||
products: [leakyProduct],
|
||||
entityId: "ent-1",
|
||||
entities,
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("matches by internal_id when entity_id not set on product", () => {
|
||||
const internalOnlyProduct = makeProduct({
|
||||
id: "cp-internal",
|
||||
entity_id: null,
|
||||
internal_entity_id: "int-ent-1",
|
||||
});
|
||||
const result = filterBySelectedEntity({
|
||||
products: [internalOnlyProduct],
|
||||
entityId: "ent-1",
|
||||
entities,
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("returns all products when entityId does not match any entity", () => {
|
||||
const products = [
|
||||
makeProduct({ id: "cp-1" }),
|
||||
makeProduct({ id: "cp-2", entity_id: "ent-1" }),
|
||||
];
|
||||
const result = filterBySelectedEntity({
|
||||
products,
|
||||
entityId: "nonexistent",
|
||||
entities,
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user