@azlib/cms 0.3.0 → 0.5.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.
package/README.md CHANGED
@@ -117,46 +117,56 @@ Create modular, decoupled plugins that extend collections, inject custom fields,
117
117
  ```typescript
118
118
  import { definePlugin, fields } from "@azlib/cms";
119
119
 
120
- export const seoPlugin = definePlugin<{ defaultTitleSuffix?: string }>((options = {}) => ({
121
- name: "seo-plugin",
122
- version: "1.0.0",
123
-
124
- // 1. Inject custom fields into existing collections
125
- extendCollections: {
126
- posts: [
127
- fields.text({ name: "metaTitle", label: "Meta Title" }),
128
- fields.text({ name: "metaDescription", label: "Meta Description" }),
129
- ],
130
- },
131
-
132
- // 2. Programmatic setup for hooks and routes
133
- setup({ hooks, registerRoute, engine }) {
134
- // Intercept content before saving
135
- hooks.addFilter("cms.before_create_input", (input: any) => {
136
- if (input.title && options.defaultTitleSuffix && !input.data?.metaTitle) {
137
- input.data = {
138
- ...input.data,
139
- metaTitle: `${input.title} | ${options.defaultTitleSuffix}`,
140
- };
141
- }
142
- return input;
143
- });
144
-
145
- // Expose custom Web Standard API route
146
- registerRoute("GET", "/api/seo/sitemap", async () => {
147
- const posts = await engine.collection("posts").find({ status: "published" });
148
- const urls = posts.items.map((p) => `https://example.com/posts/${p.slug}`);
149
- return new Response(JSON.stringify({ urls }), {
150
- headers: { "Content-Type": "application/json" },
120
+ export const seoPlugin = definePlugin<{ defaultTitleSuffix?: string }>(
121
+ (options = {}) => ({
122
+ name: "seo-plugin",
123
+ version: "1.0.0",
124
+
125
+ // 1. Inject custom fields into existing collections
126
+ extendCollections: {
127
+ posts: [
128
+ fields.text({ name: "metaTitle", label: "Meta Title" }),
129
+ fields.text({ name: "metaDescription", label: "Meta Description" }),
130
+ ],
131
+ },
132
+
133
+ // 2. Programmatic setup for hooks and routes
134
+ setup({ hooks, registerRoute, engine }) {
135
+ // Intercept content before saving
136
+ hooks.addFilter("cms.before_create_input", (input: any) => {
137
+ if (
138
+ input.title &&
139
+ options.defaultTitleSuffix &&
140
+ !input.data?.metaTitle
141
+ ) {
142
+ input.data = {
143
+ ...input.data,
144
+ metaTitle: `${input.title} | ${options.defaultTitleSuffix}`,
145
+ };
146
+ }
147
+ return input;
151
148
  });
152
- });
153
- },
154
149
 
155
- // 3. Lifecycle callbacks
156
- async onInit(engine) {
157
- console.log("[SEO Plugin] Initialized");
158
- },
159
- }));
150
+ // Expose custom Web Standard API route
151
+ registerRoute("GET", "/api/seo/sitemap", async () => {
152
+ const posts = await engine
153
+ .collection("posts")
154
+ .find({ status: "published" });
155
+ const urls = posts.items.map(
156
+ (p) => `https://example.com/posts/${p.slug}`,
157
+ );
158
+ return new Response(JSON.stringify({ urls }), {
159
+ headers: { "Content-Type": "application/json" },
160
+ });
161
+ });
162
+ },
163
+
164
+ // 3. Lifecycle callbacks
165
+ async onInit(engine) {
166
+ console.log("[SEO Plugin] Initialized");
167
+ },
168
+ }),
169
+ );
160
170
  ```
161
171
 
162
172
  Register plugins declaratively in `defineConfig` or dynamically via `cms.use()`:
@@ -173,6 +183,215 @@ cms.use(seoPlugin());
173
183
 
174
184
  ---
175
185
 
186
+ ## Built-in E-commerce Plugin
187
+
188
+ Turn `@azlib/cms` into a complete commerce backend with pre-defined schemas, catalog taxonomies, image uploading, coupon validation, cart calculation, inventory tracking, and orders:
189
+
190
+ ```typescript
191
+ import {
192
+ createCMSEngine,
193
+ defineConfig,
194
+ ecommercePlugin,
195
+ getEcommerceService,
196
+ } from "@azlib/cms";
197
+
198
+ // 1. Enable the plugin in your config
199
+ export default defineConfig({
200
+ plugins: [
201
+ ecommercePlugin({
202
+ defaultCurrency: "USD",
203
+ inventoryManagement: true,
204
+ defaultShippingCost: 15,
205
+ defaultTaxRate: 0.08, // 8%
206
+ }),
207
+ ],
208
+ });
209
+ ```
210
+
211
+ ### 1. Catalog & Product Management
212
+
213
+ ```typescript
214
+ const cms = createCMSEngine();
215
+ cms.use(ecommercePlugin());
216
+ await cms.init();
217
+
218
+ const commerce = getEcommerceService(cms);
219
+
220
+ // Create hierarchical catalog category
221
+ const apparel = await commerce.createCategory({
222
+ name: "Apparel",
223
+ slug: "apparel",
224
+ });
225
+ const shoes = await commerce.createCategory({
226
+ name: "Shoes",
227
+ slug: "shoes",
228
+ parentId: apparel.id,
229
+ });
230
+
231
+ // Create product with SKU, pricing, inventory, variants
232
+ const product = await commerce.createProduct({
233
+ title: "Aero Running Shoes",
234
+ price: 139.99,
235
+ compareAtPrice: 169.99,
236
+ sku: "RUN-AERO-01",
237
+ stock: 25,
238
+ status: "published",
239
+ categoryIds: [shoes.id],
240
+ variants: [
241
+ { id: "v-42", title: "Size 42", price: 139.99, stock: 10 },
242
+ { id: "v-43", title: "Size 43", price: 139.99, stock: 15 },
243
+ ],
244
+ });
245
+
246
+ // Upload and attach product images (auto-updates featured image and gallery)
247
+ await commerce.uploadProductImage(product.id, {
248
+ filename: "aero-shoes.jpg",
249
+ mimeType: "image/jpeg",
250
+ sizeBytes: 1048576,
251
+ url: "https://example.com/uploads/aero-shoes.jpg",
252
+ isFeatured: true,
253
+ });
254
+ ```
255
+
256
+ ### 2. Discounts & Coupon Engine
257
+
258
+ ```typescript
259
+ // Create promotional coupon
260
+ await commerce.createDiscount({
261
+ title: "Summer 20%",
262
+ code: "SUMMER20",
263
+ discountType: "percentage",
264
+ value: 20,
265
+ minOrderAmount: 50,
266
+ maxDiscountAmount: 30,
267
+ });
268
+
269
+ // Validate discount for a cart
270
+ const validation = await commerce.validateDiscount("SUMMER20", 100);
271
+ // => { valid: true, discountAmount: 20, code: "SUMMER20" }
272
+ ```
273
+
274
+ ### 3. Cart Calculation & Orders
275
+
276
+ ```typescript
277
+ // Calculate totals (items, discounts, shipping, and tax)
278
+ const cart = await commerce.calculateCart({
279
+ items: [{ productId: product.id, quantity: 2 }],
280
+ discountCode: "SUMMER20",
281
+ });
282
+
283
+ // Place an order (automatically decrements inventory and increments coupon counter)
284
+ const order = await commerce.createOrder({
285
+ customerEmail: "customer@example.com",
286
+ customerName: "Jane Doe",
287
+ items: [{ productId: product.id, quantity: 2 }],
288
+ discountCode: "SUMMER20",
289
+ shippingAddress: {
290
+ address1: "456 Market St",
291
+ city: "San Francisco",
292
+ country: "US",
293
+ postalCode: "94103",
294
+ },
295
+ });
296
+
297
+ console.log(
298
+ `Order placed: #${order.data.orderNumber} (Total: $${order.data.total})`,
299
+ );
300
+ ```
301
+
302
+ ### 4. REST API & Headless Client
303
+
304
+ Mount the universal `createCMSRouter(cms)` to automatically expose REST endpoints:
305
+
306
+ - `GET /api/ecommerce/products` (supports `?category=...&minPrice=...&maxPrice=...&inStock=true&search=...`)
307
+ - `GET /api/ecommerce/products/:idOrSlug`
308
+ - `POST /api/ecommerce/products`
309
+ - `POST /api/ecommerce/products/:id/images`
310
+ - `GET /api/ecommerce/categories` (supports `?tree=true`)
311
+ - `POST /api/ecommerce/discounts/validate`
312
+ - `POST /api/ecommerce/cart/calculate`
313
+ - `POST /api/ecommerce/orders`
314
+ - `PATCH /api/ecommerce/orders/:id/status`
315
+
316
+ Or query via the Headless Client SDK:
317
+
318
+ ```typescript
319
+ import { createCmsClient, getEcommerceClient } from "@azlib/cms";
320
+
321
+ const client = createCmsClient({ baseUrl: "https://api.my-shop.com" });
322
+ const shop = getEcommerceClient(client);
323
+
324
+ const { items: products } = await shop.products.find({ minPrice: 50 });
325
+ const order = await shop.orders.create({ ... });
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Built-in HRMS Plugin (`hrmsPlugin`)
331
+
332
+ `@azlib/cms` includes a full-featured Human Resource Management System (HRMS) plugin for multi-tenant companies, employee directory profiles, attendance tracking, and leave quota management.
333
+
334
+ ### Features
335
+
336
+ - 🏢 **Multi-Tenant Employers**: Manage distinct companies/organizations with custom work schedules, timezones, and grace periods.
337
+ - 👤 **Employee Directory**: Rich employee profiles, contract/document attachments, emergency contacts, manager hierarchies, and department taxonomies.
338
+ - ⏱️ **Daily Attendance Tracking**: Clock-in and clock-out with automated duration calculation, overtime hours, and late-arrival detection against work schedules.
339
+ - 🏖️ **Leave Quota & Approval Workflow**: Custom leave types (Annual, Sick, Unpaid), balance ledger reports, and manager approval/rejection pipelines.
340
+ - 🌐 **Web Standard REST APIs & Client SDK**: Pre-mounted routes under `/api/hrms/*` and typed `HRMSClient` SDK.
341
+
342
+ ### Usage
343
+
344
+ ```typescript
345
+ import {
346
+ createCMSEngine,
347
+ defineConfig,
348
+ hrmsPlugin,
349
+ getHRMSService,
350
+ } from "@azlib/cms";
351
+
352
+ const config = defineConfig({
353
+ plugins: [
354
+ hrmsPlugin({
355
+ workScheduleStart: "09:00",
356
+ workScheduleEnd: "17:00",
357
+ standardWorkDayHours: 8,
358
+ gracePeriodMinutes: 15,
359
+ }),
360
+ ],
361
+ });
362
+
363
+ const cms = createCMSEngine(config);
364
+ await cms.init();
365
+
366
+ const hrms = getHRMSService(cms);
367
+
368
+ // 1. Create an Employer
369
+ const employer = await hrms.createEmployer({
370
+ companyName: "Acme Corp",
371
+ timezone: "America/New_York",
372
+ });
373
+
374
+ // 2. Add an Employee
375
+ const employee = await hrms.createEmployee({
376
+ employerId: employer.id,
377
+ employeeNumber: "ACME-001",
378
+ firstName: "Jane",
379
+ lastName: "Doe",
380
+ email: "jane.doe@acme.example",
381
+ hireDate: "2024-01-15",
382
+ });
383
+
384
+ // 3. Employee Check-In & Check-Out
385
+ await hrms.checkIn({ employeeId: employee.id });
386
+ await hrms.checkOut({ employeeId: employee.id });
387
+
388
+ // 4. Request Leave & Check Balance
389
+ const leaveReport = await hrms.calculateLeaveBalance(employee.id, 2026);
390
+ console.log(`Remaining days: ${leaveReport.totalRemaining}`);
391
+ ```
392
+
393
+ ---
394
+
176
395
  ## License
177
396
 
178
397
  MIT © Google / azlib