152 lines
4.9 KiB
TypeScript
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}`;
|
|
};
|
|
}
|
|
}
|