@richardmcquiston01/online-catalog-cms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,558 @@
1
+ import { Readable } from 'node:stream';
2
+
3
+ /** A text node with optional inline formatting. */
4
+ interface TextNode {
5
+ type: 'text';
6
+ text: string;
7
+ bold?: boolean;
8
+ italic?: boolean;
9
+ code?: boolean;
10
+ }
11
+ /** An inline hyperlink containing child inline nodes. */
12
+ interface LinkNode {
13
+ type: 'link';
14
+ href: string;
15
+ children: InlineNode[];
16
+ }
17
+ /** Union of all inline content nodes. */
18
+ type InlineNode = TextNode | LinkNode;
19
+ /** A paragraph block. */
20
+ interface ParagraphNode {
21
+ type: 'paragraph';
22
+ children: InlineNode[];
23
+ cssClass?: string;
24
+ }
25
+ /** A heading block (h1–h6). */
26
+ interface HeadingNode {
27
+ type: 'heading';
28
+ level: 1 | 2 | 3 | 4 | 5 | 6;
29
+ children: InlineNode[];
30
+ cssClass?: string;
31
+ }
32
+ /** An inline image embedded in the document. */
33
+ interface RichTextImageNode {
34
+ type: 'image';
35
+ src: string;
36
+ alt: string;
37
+ cssClass?: string;
38
+ }
39
+ /** An ordered or unordered list. Each item is an array of inline nodes. */
40
+ interface ListNode {
41
+ type: 'list';
42
+ ordered: boolean;
43
+ items: InlineNode[][];
44
+ cssClass?: string;
45
+ }
46
+ /** A block-level quote. */
47
+ interface BlockquoteNode {
48
+ type: 'blockquote';
49
+ children: InlineNode[];
50
+ cssClass?: string;
51
+ }
52
+ /** Union of all block-level content nodes. */
53
+ type RichTextNode = ParagraphNode | HeadingNode | RichTextImageNode | ListNode | BlockquoteNode;
54
+ /**
55
+ * The JSON document schema for rich-text descriptions.
56
+ * CSS classes are per-node so consumers control styling without a theming API.
57
+ */
58
+ interface RichTextDocument {
59
+ version: 1;
60
+ nodes: RichTextNode[];
61
+ }
62
+
63
+ /**
64
+ * A product category. Categories are hierarchical: a category may have a
65
+ * `parentId` pointing to another category.
66
+ */
67
+ interface Category {
68
+ id: string;
69
+ name: string;
70
+ slug: string;
71
+ parentId: string | null;
72
+ metadata: Record<string, unknown>;
73
+ createdAt: Date;
74
+ updatedAt: Date;
75
+ }
76
+ /** Input for creating a new category. */
77
+ interface CreateCategoryInput {
78
+ name: string;
79
+ slug?: string;
80
+ parentId?: string | null;
81
+ metadata?: Record<string, unknown>;
82
+ }
83
+ /** Fields that may be updated on an existing category. */
84
+ interface UpdateCategoryInput {
85
+ name?: string;
86
+ slug?: string;
87
+ parentId?: string | null;
88
+ metadata?: Record<string, unknown>;
89
+ }
90
+ /** Filters for listing categories. */
91
+ interface CategoryFilter {
92
+ parentId?: string | null;
93
+ search?: string;
94
+ limit?: number;
95
+ offset?: number;
96
+ }
97
+
98
+ /** A media image associated with a product. */
99
+ interface Image {
100
+ id: string;
101
+ productId: string;
102
+ url: string;
103
+ altText: string;
104
+ sortOrder: number;
105
+ createdAt: Date;
106
+ }
107
+ /** Input for associating a new image with a product. */
108
+ interface CreateImageInput {
109
+ productId: string;
110
+ url: string;
111
+ altText: string;
112
+ sortOrder?: number;
113
+ }
114
+
115
+ /**
116
+ * A catalog product. `price` is stored in the smallest currency unit
117
+ * (e.g. cents) to avoid floating-point rounding issues.
118
+ */
119
+ interface Product {
120
+ id: string;
121
+ name: string;
122
+ slug: string;
123
+ description: RichTextDocument;
124
+ /** Price in smallest currency unit (e.g. cents). */
125
+ price: number;
126
+ sku: string | null;
127
+ categoryId: string | null;
128
+ images: Image[];
129
+ metadata: Record<string, unknown>;
130
+ createdAt: Date;
131
+ updatedAt: Date;
132
+ }
133
+ /** Input for creating a new product. */
134
+ interface CreateProductInput {
135
+ name: string;
136
+ description: RichTextDocument;
137
+ price: number;
138
+ slug?: string;
139
+ sku?: string | null;
140
+ categoryId?: string | null;
141
+ metadata?: Record<string, unknown>;
142
+ }
143
+ /** Fields that may be updated on an existing product. */
144
+ interface UpdateProductInput {
145
+ name?: string;
146
+ slug?: string;
147
+ description?: RichTextDocument;
148
+ price?: number;
149
+ sku?: string | null;
150
+ categoryId?: string | null;
151
+ metadata?: Record<string, unknown>;
152
+ }
153
+ /** Filters for listing products. */
154
+ interface ProductFilter {
155
+ categoryId?: string | null;
156
+ search?: string;
157
+ minPrice?: number;
158
+ maxPrice?: number;
159
+ limit?: number;
160
+ offset?: number;
161
+ }
162
+
163
+ /** Result returned by adapter.verify(). */
164
+ interface VerificationResult {
165
+ ok: boolean;
166
+ issues: string[];
167
+ }
168
+ /** CRUD operations for products. */
169
+ interface ProductRepository {
170
+ create(input: CreateProductInput): Promise<Product>;
171
+ get(id: string): Promise<Product | null>;
172
+ update(id: string, input: UpdateProductInput): Promise<Product>;
173
+ delete(id: string): Promise<void>;
174
+ list(filter?: ProductFilter): Promise<Product[]>;
175
+ }
176
+ /** CRUD operations for categories. */
177
+ interface CategoryRepository {
178
+ create(input: CreateCategoryInput): Promise<Category>;
179
+ get(id: string): Promise<Category | null>;
180
+ update(id: string, input: UpdateCategoryInput): Promise<Category>;
181
+ delete(id: string): Promise<void>;
182
+ list(filter?: CategoryFilter): Promise<Category[]>;
183
+ }
184
+ /** CRUD operations for images. */
185
+ interface ImageRepository {
186
+ create(input: CreateImageInput): Promise<Image>;
187
+ get(id: string): Promise<Image | null>;
188
+ delete(id: string): Promise<void>;
189
+ listByProduct(productId: string): Promise<Image[]>;
190
+ }
191
+ /**
192
+ * Contract that every database adapter must implement.
193
+ * Consumers pass an instance of a concrete adapter to `OnlineCatalog`.
194
+ */
195
+ interface DatabaseAdapter {
196
+ /** Runs any pending migrations, creating schema on first run. */
197
+ initialize(): Promise<void>;
198
+ /** Checks that the schema matches what the library expects. */
199
+ verify(): Promise<VerificationResult>;
200
+ /** Releases all connections and cleans up resources. */
201
+ close(): Promise<void>;
202
+ readonly products: ProductRepository;
203
+ readonly categories: CategoryRepository;
204
+ readonly images: ImageRepository;
205
+ }
206
+
207
+ /** Options passed to a storage adapter's upload method. */
208
+ interface UploadOptions {
209
+ /** MIME type of the file, e.g. "image/jpeg". */
210
+ contentType?: string;
211
+ /** Whether the file should be publicly readable. Defaults to true. */
212
+ public?: boolean;
213
+ }
214
+ /**
215
+ * Contract that every storage adapter must implement.
216
+ * Consumers pass an instance to `OnlineCatalog` for image/file handling.
217
+ */
218
+ interface StorageAdapter {
219
+ /**
220
+ * Upload a file and return its public URL.
221
+ * @param file - File content as a Buffer or Readable stream.
222
+ * @param filename - Desired filename (may be modified by the adapter).
223
+ * @param options - Optional upload metadata.
224
+ * @returns The public URL at which the file can be accessed.
225
+ */
226
+ upload(file: Buffer | Readable, filename: string, options?: UploadOptions): Promise<string>;
227
+ /**
228
+ * Delete a file by its URL.
229
+ * No-ops silently if the file does not exist.
230
+ */
231
+ delete(url: string): Promise<void>;
232
+ /**
233
+ * Derive the public URL for a storage key without making a network call.
234
+ * Useful for building URLs after listing keys from the storage backend.
235
+ */
236
+ getPublicUrl(key: string): string;
237
+ }
238
+
239
+ /** Create a plain text inline node. */
240
+ declare function text(content: string, options?: {
241
+ bold?: boolean;
242
+ italic?: boolean;
243
+ code?: boolean;
244
+ }): TextNode;
245
+ /** Create a hyperlink inline node. */
246
+ declare function link(href: string, children: InlineNode[]): LinkNode;
247
+ /** Create a paragraph block node. */
248
+ declare function paragraph(children: InlineNode[], cssClass?: string): ParagraphNode;
249
+ /** Create a heading block node (h1–h6). */
250
+ declare function heading(level: 1 | 2 | 3 | 4 | 5 | 6, children: InlineNode[], cssClass?: string): HeadingNode;
251
+ /** Create an inline image node. */
252
+ declare function image(src: string, alt: string, cssClass?: string): RichTextImageNode;
253
+ /** Create an unordered list node. */
254
+ declare function unorderedList(items: InlineNode[][], cssClass?: string): ListNode;
255
+ /** Create an ordered list node. */
256
+ declare function orderedList(items: InlineNode[][], cssClass?: string): ListNode;
257
+ /** Create a blockquote node. */
258
+ declare function blockquote(children: InlineNode[], cssClass?: string): BlockquoteNode;
259
+ /** Wrap an array of block nodes into a complete RichTextDocument. */
260
+ declare function document(nodes: RichTextDocument['nodes']): RichTextDocument;
261
+
262
+ /**
263
+ * Returns true if `value` is a valid {@link RichTextDocument}.
264
+ * Use this at system boundaries (e.g. API input) before persisting content.
265
+ */
266
+ declare function isRichTextDocument(value: unknown): value is RichTextDocument;
267
+ /**
268
+ * Throws a `TypeError` if `value` is not a valid {@link RichTextDocument}.
269
+ */
270
+ declare function assertRichTextDocument(value: unknown): asserts value is RichTextDocument;
271
+
272
+ interface InstallOptions {
273
+ /** If true, describe what would happen without making changes. */
274
+ dryRun?: boolean;
275
+ }
276
+ /** Wraps adapter initialization and verification with helpful logging. */
277
+ declare class Installer {
278
+ private readonly adapter;
279
+ constructor(adapter: DatabaseAdapter);
280
+ /**
281
+ * Run database migrations and verify the resulting schema.
282
+ * @returns The verification result after migration.
283
+ */
284
+ install(options?: InstallOptions): Promise<VerificationResult>;
285
+ /** Check that the schema is correct without running migrations. */
286
+ verify(): Promise<VerificationResult>;
287
+ }
288
+
289
+ /** High-level category operations. */
290
+ declare class CategoryService {
291
+ private readonly repo;
292
+ constructor(repo: CategoryRepository);
293
+ create(input: CreateCategoryInput): Promise<Category>;
294
+ get(id: string): Promise<Category | null>;
295
+ update(id: string, input: UpdateCategoryInput): Promise<Category>;
296
+ delete(id: string): Promise<void>;
297
+ list(filter?: CategoryFilter): Promise<Category[]>;
298
+ }
299
+
300
+ interface UploadImageInput {
301
+ productId: string;
302
+ file: Buffer | Readable;
303
+ filename: string;
304
+ altText: string;
305
+ contentType?: string;
306
+ sortOrder?: number;
307
+ }
308
+ /** Handles image uploads and associates them with products. */
309
+ declare class ImageService {
310
+ private readonly repo;
311
+ private readonly storage?;
312
+ constructor(repo: ImageRepository, storage?: StorageAdapter | undefined);
313
+ /**
314
+ * Upload a file via the storage adapter and create an image record.
315
+ * Requires a storage adapter to be configured.
316
+ */
317
+ upload(input: UploadImageInput): Promise<Image>;
318
+ /** Associate an already-hosted image URL with a product. */
319
+ addUrl(input: CreateImageInput): Promise<Image>;
320
+ get(id: string): Promise<Image | null>;
321
+ delete(id: string): Promise<void>;
322
+ listByProduct(productId: string): Promise<Image[]>;
323
+ }
324
+
325
+ /** High-level product operations. Wraps the DB repository with slug generation. */
326
+ declare class ProductService {
327
+ private readonly repo;
328
+ private readonly storage?;
329
+ constructor(repo: ProductRepository, storage?: StorageAdapter | undefined);
330
+ create(input: CreateProductInput): Promise<Product>;
331
+ get(id: string): Promise<Product | null>;
332
+ update(id: string, input: UpdateProductInput): Promise<Product>;
333
+ delete(id: string): Promise<void>;
334
+ list(filter?: ProductFilter): Promise<Product[]>;
335
+ }
336
+
337
+ interface OnlineCatalogConfig {
338
+ /** Database adapter for persisting catalog data. */
339
+ db: DatabaseAdapter;
340
+ /** Storage adapter for image/file uploads. Optional. */
341
+ storage?: StorageAdapter;
342
+ }
343
+ /**
344
+ * Main entry point for online-catalog-cms.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * const catalog = new OnlineCatalog({ db: new SQLiteAdapter({ filename: './catalog.db' }) });
349
+ * await catalog.initialize();
350
+ * const product = await catalog.products.create({ name: 'Widget', price: 999, description: ... });
351
+ * ```
352
+ */
353
+ declare class OnlineCatalog {
354
+ readonly products: ProductService;
355
+ readonly categories: CategoryService;
356
+ readonly images: ImageService;
357
+ readonly installer: Installer;
358
+ private readonly db;
359
+ constructor(config: OnlineCatalogConfig);
360
+ /**
361
+ * Initialize the catalog. Runs database migrations on first use.
362
+ * Must be called before any other method.
363
+ */
364
+ initialize(): Promise<void>;
365
+ /** Release all resources held by the database adapter. */
366
+ close(): Promise<void>;
367
+ }
368
+
369
+ interface SQLRunner {
370
+ /** Execute a statement that returns no rows. */
371
+ run(sql: string, params?: unknown[]): Promise<void>;
372
+ /** Execute a query and return all rows. */
373
+ all<T>(sql: string, params?: unknown[]): Promise<T[]>;
374
+ /** Execute a query and return the first row, or undefined. */
375
+ get<T>(sql: string, params?: unknown[]): Promise<T | undefined>;
376
+ }
377
+ /**
378
+ * Shared logic for relational SQL adapters: migration runner and schema
379
+ * verification. Concrete adapters extend this and supply a `SQLRunner`.
380
+ */
381
+ declare abstract class BaseSQLAdapter {
382
+ protected abstract readonly db: SQLRunner;
383
+ /** Dialect-specific table existence query — must return rows with a `name` column. */
384
+ protected abstract tableExistsQuery(table: string): string;
385
+ protected abstract get migrationSql(): string;
386
+ initialize(): Promise<void>;
387
+ verify(): Promise<VerificationResult>;
388
+ }
389
+
390
+ interface SQLiteConfig {
391
+ /** Path to the SQLite database file. Use ':memory:' for an in-memory DB. */
392
+ filename: string;
393
+ }
394
+ /** SQLite database adapter. Uses bun:sqlite in Bun, better-sqlite3 in Node.js. */
395
+ declare class SQLiteAdapter extends BaseSQLAdapter implements DatabaseAdapter {
396
+ private runner?;
397
+ private closeHandle?;
398
+ private readonly config;
399
+ private _products?;
400
+ private _categories?;
401
+ private _images?;
402
+ constructor(config: SQLiteConfig);
403
+ protected get db(): SQLRunner;
404
+ get products(): ProductRepository;
405
+ get categories(): CategoryRepository;
406
+ get images(): ImageRepository;
407
+ /** Must be called before any other method. Sets up the SQLite driver. */
408
+ initialize(): Promise<void>;
409
+ protected get migrationSql(): string;
410
+ protected tableExistsQuery(table: string): string;
411
+ verify(): Promise<VerificationResult>;
412
+ close(): Promise<void>;
413
+ }
414
+
415
+ interface PostgresConfig {
416
+ /** postgres.js connection string or config object. */
417
+ url: string;
418
+ }
419
+ /** PostgreSQL database adapter using postgres.js. */
420
+ declare class PostgresAdapter extends BaseSQLAdapter implements DatabaseAdapter {
421
+ protected readonly db: SQLRunner;
422
+ readonly products: ProductRepository;
423
+ readonly categories: CategoryRepository;
424
+ readonly images: ImageRepository;
425
+ private readonly sql;
426
+ constructor(config: PostgresConfig);
427
+ protected get migrationSql(): string;
428
+ protected tableExistsQuery(table: string): string;
429
+ verify(): Promise<VerificationResult>;
430
+ close(): Promise<void>;
431
+ }
432
+
433
+ interface MySQLConfig {
434
+ host: string;
435
+ port?: number;
436
+ user: string;
437
+ password: string;
438
+ database: string;
439
+ }
440
+ /** MySQL / MariaDB adapter using mysql2. */
441
+ declare class MySQLAdapter extends BaseSQLAdapter implements DatabaseAdapter {
442
+ protected readonly db: SQLRunner;
443
+ readonly products: ProductRepository;
444
+ readonly categories: CategoryRepository;
445
+ readonly images: ImageRepository;
446
+ private readonly pool;
447
+ constructor(config: MySQLConfig);
448
+ protected get migrationSql(): string;
449
+ protected tableExistsQuery(table: string): string;
450
+ verify(): Promise<VerificationResult>;
451
+ close(): Promise<void>;
452
+ }
453
+
454
+ interface RedisConfig {
455
+ /** ioredis connection URL, e.g. 'redis://localhost:6379'. */
456
+ url: string;
457
+ /** Optional key prefix to namespace all catalog keys. Defaults to 'occ'. */
458
+ keyPrefix?: string;
459
+ }
460
+ /**
461
+ * Redis adapter. Data is stored as JSON strings in Redis hashes.
462
+ *
463
+ * Key layout:
464
+ * {prefix}:product:{id} — product JSON
465
+ * {prefix}:products:all — set of all product IDs
466
+ * {prefix}:products:cat:{catId} — set of product IDs by category
467
+ * {prefix}:category:{id} — category JSON
468
+ * {prefix}:categories:all — set of all category IDs
469
+ * {prefix}:image:{id} — image JSON
470
+ * {prefix}:images:product:{pid} — sorted set of image IDs by sort_order
471
+ */
472
+ declare class RedisAdapter implements DatabaseAdapter {
473
+ private readonly client;
474
+ private readonly prefix;
475
+ readonly products: ProductRepository;
476
+ readonly categories: CategoryRepository;
477
+ readonly images: ImageRepository;
478
+ constructor(config: RedisConfig);
479
+ initialize(): Promise<void>;
480
+ verify(): Promise<VerificationResult>;
481
+ close(): Promise<void>;
482
+ }
483
+
484
+ interface MongoDBConfig {
485
+ /** MongoDB connection string, e.g. 'mongodb://localhost:27017'. */
486
+ url: string;
487
+ /** Database name. Defaults to 'online_catalog'. */
488
+ database?: string;
489
+ }
490
+ /** MongoDB adapter. Each entity type maps to its own collection. */
491
+ declare class MongoDBAdapter implements DatabaseAdapter {
492
+ private readonly client;
493
+ private readonly dbName;
494
+ readonly products: ProductRepository;
495
+ readonly categories: CategoryRepository;
496
+ readonly images: ImageRepository;
497
+ constructor(config: MongoDBConfig);
498
+ initialize(): Promise<void>;
499
+ verify(): Promise<VerificationResult>;
500
+ close(): Promise<void>;
501
+ }
502
+
503
+ interface LocalStorageConfig {
504
+ /** Absolute path to the directory where uploaded files will be stored. */
505
+ uploadDir: string;
506
+ /**
507
+ * Base URL prefix for returned file URLs, e.g. 'http://localhost:3000/uploads'.
508
+ * Defaults to '/uploads' (a relative path).
509
+ */
510
+ baseUrl?: string;
511
+ }
512
+ /** Storage adapter that writes files to the local filesystem. */
513
+ declare class LocalStorageAdapter implements StorageAdapter {
514
+ private readonly uploadDir;
515
+ private readonly baseUrl;
516
+ constructor(config: LocalStorageConfig);
517
+ upload(file: Buffer | Readable, filename: string, _options?: UploadOptions): Promise<string>;
518
+ delete(url: string): Promise<void>;
519
+ getPublicUrl(key: string): string;
520
+ private urlToKey;
521
+ }
522
+
523
+ interface S3Config {
524
+ bucket: string;
525
+ region: string;
526
+ /** Custom endpoint for S3-compatible services (MinIO, Cloudflare R2, etc.). */
527
+ endpoint?: string;
528
+ accessKeyId: string;
529
+ secretAccessKey: string;
530
+ /** Whether objects should default to public-read ACL. Defaults to true. */
531
+ publicRead?: boolean;
532
+ }
533
+ /** S3-compatible storage adapter using @aws-sdk/client-s3. */
534
+ declare class S3Adapter implements StorageAdapter {
535
+ private readonly client;
536
+ private readonly bucket;
537
+ private readonly publicRead;
538
+ private readonly baseUrl;
539
+ private readonly aws;
540
+ constructor(config: S3Config);
541
+ upload(file: Buffer | Readable, filename: string, options?: UploadOptions): Promise<string>;
542
+ delete(url: string): Promise<void>;
543
+ getPublicUrl(key: string): string;
544
+ private urlToKey;
545
+ }
546
+
547
+ /**
548
+ * Storage adapter for externally-hosted images.
549
+ * `upload` is not supported — pass URLs directly in `CreateImageInput.url`
550
+ * and this adapter stores them as-is.
551
+ */
552
+ declare class ExternalURLAdapter implements StorageAdapter {
553
+ upload(_file: Buffer | Readable, _filename: string, _options?: UploadOptions): Promise<string>;
554
+ delete(_url: string): Promise<void>;
555
+ getPublicUrl(key: string): string;
556
+ }
557
+
558
+ export { type BlockquoteNode, type Category, type CategoryFilter, type CategoryRepository, type CreateCategoryInput, type CreateImageInput, type CreateProductInput, type DatabaseAdapter, ExternalURLAdapter, type HeadingNode, type Image, type ImageRepository, type InlineNode, type InstallOptions, Installer, type LinkNode, type ListNode, LocalStorageAdapter, type LocalStorageConfig, MongoDBAdapter, type MongoDBConfig, MySQLAdapter, type MySQLConfig, OnlineCatalog, type OnlineCatalogConfig, type ParagraphNode, PostgresAdapter, type PostgresConfig, type Product, type ProductFilter, type ProductRepository, RedisAdapter, type RedisConfig, type RichTextDocument, type RichTextImageNode, type RichTextNode, S3Adapter, type S3Config, SQLiteAdapter, type SQLiteConfig, type StorageAdapter, type TextNode, type UpdateCategoryInput, type UpdateProductInput, type UploadOptions, type VerificationResult, assertRichTextDocument, blockquote, document, heading, image, isRichTextDocument, link, orderedList, paragraph, text, unorderedList };