Files
suaveplan/packages/commerce/contacts/audience-rules/src/store/store.ts
2026-05-27 23:29:53 +01:00

152 lines
4.9 KiB
TypeScript

import type { Clock, IdGenerator } from "@suaveplan/types";
import {
AUDIENCE_RULES_CODES,
AudienceRulesError,
} from "../error-codes/error-codes.js";
import {
createSegmentInputSchema,
parseOrThrow,
updateSegmentInputSchema,
} from "../schemas/schemas.js";
import type {
CreateSegmentInput,
Segment,
UpdateSegmentInput,
} from "../types/segment.types.js";
/** Interface for segment persistence. */
export interface SegmentStore {
create(input: CreateSegmentInput): Promise<Segment>;
getById(id: string): Promise<Segment | undefined>;
update(id: string, input: UpdateSegmentInput): Promise<Segment | undefined>;
delete(id: string): Promise<boolean>;
list(tenantId: string): Promise<readonly Segment[]>;
updateCache(
id: string,
contactIds: readonly string[]
): Promise<Segment | undefined>;
}
/** Constructor options for {@link InMemorySegmentStore}. */
export interface InMemorySegmentStoreOptions {
/** Injected clock; defaults to `() => Date.now()`. */
readonly clock?: Clock;
/** Injected ID generator; defaults to a deterministic per-instance counter. */
readonly idGenerator?: IdGenerator;
}
/**
* In-memory `SegmentStore` implementation suitable for development, tests, and
* lightweight production use. Uses `Clock` + `IdGenerator` injection from
* `@suaveplan/types` — production paths never call `Date.now()` / `new Date()`
* directly and never mutate a module-scope counter.
*/
export class InMemorySegmentStore implements SegmentStore {
private readonly segments = new Map<string, Segment>();
private readonly clock: Clock;
private readonly idGenerator: IdGenerator;
public constructor(options: InMemorySegmentStoreOptions = {}) {
this.clock = options.clock ?? (() => Date.now());
this.idGenerator = options.idGenerator ?? this.makeDefaultIdGenerator();
}
public async create(input: CreateSegmentInput): Promise<Segment> {
const validated = parseOrThrow(
createSegmentInputSchema,
input,
"createSegment"
);
const now = new Date(this.clock());
const segment: Segment = {
id: this.idGenerator(),
tenantId: validated.tenantId,
name: validated.name,
description: validated.description,
rules: validated.rules,
refreshMode: validated.refreshMode,
createdAt: now,
updatedAt: now,
};
this.segments.set(segment.id, segment);
return segment;
}
public async getById(id: string): Promise<Segment | undefined> {
return this.segments.get(id);
}
public async update(
id: string,
input: UpdateSegmentInput
): Promise<Segment | undefined> {
const validated = parseOrThrow(
updateSegmentInputSchema,
input,
"updateSegment"
);
const existing = this.segments.get(id);
if (existing === undefined) return undefined;
const updated: Segment = {
...existing,
...(validated.name !== undefined && { name: validated.name }),
...(validated.description !== undefined && {
description: validated.description,
}),
...(validated.rules !== undefined && { rules: validated.rules }),
...(validated.refreshMode !== undefined && {
refreshMode: validated.refreshMode,
}),
updatedAt: new Date(this.clock()),
};
this.segments.set(id, updated);
return updated;
}
public async delete(id: string): Promise<boolean> {
return this.segments.delete(id);
}
public async list(tenantId: string): Promise<readonly Segment[]> {
if (tenantId.length === 0) {
throw new AudienceRulesError(
AUDIENCE_RULES_CODES.INVALID_INPUT,
"list requires a non-empty tenantId",
{ tenantId }
);
}
const results: Segment[] = [];
for (const segment of this.segments.values()) {
if (segment.tenantId === tenantId) results.push(segment);
}
return results;
}
public async updateCache(
id: string,
contactIds: readonly string[]
): Promise<Segment | undefined> {
const existing = this.segments.get(id);
if (existing === undefined) return undefined;
const now = new Date(this.clock());
const updated: Segment = {
...existing,
cachedContactIds: contactIds,
cachedCount: contactIds.length,
lastRefreshedAt: now,
updatedAt: now,
};
this.segments.set(id, updated);
return updated;
}
private makeDefaultIdGenerator(): IdGenerator {
let counter = 0;
return () => {
counter += 1;
return `seg_${counter}`;
};
}
}