Initial Commit
This commit is contained in:
151
packages/commerce/contacts/audience-rules/src/store/store.ts
Normal file
151
packages/commerce/contacts/audience-rules/src/store/store.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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}`;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user