diff --git a/MIGRATION_v2.0.0.md b/MIGRATION_v2.0.0.md new file mode 100644 index 000000000..31e11846e --- /dev/null +++ b/MIGRATION_v2.0.0.md @@ -0,0 +1,247 @@ +# Migration Guide: @objectstack v1.1.0 โ†’ v2.0.0 + +This document describes the breaking changes and migration steps when upgrading from @objectstack v1.1.0 to v2.0.0. + +## Summary + +@objectstack v2.0.0 introduces a breaking change to the Hook API. The `HookContext` interface has been redesigned to better align with ObjectQL lifecycle events and provide clearer semantics. + +## Breaking Changes + +### 1. Hook Context API Changes + +The `HookContext` interface properties have changed: + +#### Before (v1.1.0) +```typescript +interface HookContext { + new: Record; // The new/current document + old?: Record; // The previous document (for updates/deletes) + // ... other properties +} + +const hook: Hook = { + name: 'MyHook', + events: ['afterUpdate'], + handler: async (ctx) => { + const newDoc = ctx.new; + const oldDoc = ctx.old; + if (newDoc.status === 'Active' && oldDoc.status !== 'Active') { + // Do something + } + } +}; +``` + +#### After (v2.0.0) +```typescript +interface HookContext { + input: Record; // Contains { doc: ... } for insert/update + result?: unknown; // The operation result (after* events) + previous?: Record; // The previous state (update/delete) + ql: unknown; // ObjectQL engine reference + // ... other properties +} + +const hook: Hook = { + name: 'MyHook', + events: ['afterUpdate'], + handler: async (ctx: any) => { + const newDoc = ctx.result as any; + const oldDoc = ctx.previous as any; + if (newDoc?.status === 'Active' && oldDoc?.status !== 'Active') { + // Do something + } + } +}; +``` + +## Migration Rules + +### For `beforeInsert` Events +- **v1.1.0**: `ctx.new` contained the document being inserted +- **v2.0.0**: `ctx.input.doc` contains the document being inserted +- Mutations should be applied to `ctx.input.doc` + +```typescript +// v1.1.0 +handler: async (ctx) => { + ctx.new.created_date = new Date().toISOString(); +} + +// v2.0.0 +handler: async (ctx: any) => { + (ctx.input.doc as any).created_date = new Date().toISOString(); +} +``` + +### For `afterInsert` Events +- **v1.1.0**: `ctx.new` contained the inserted document +- **v2.0.0**: `ctx.result` contains the inserted document + +```typescript +// v1.1.0 +handler: async (ctx) => { + const doc = ctx.new; + console.log(`Created document ${doc._id}`); +} + +// v2.0.0 +handler: async (ctx: any) => { + const doc = ctx.result as any; + console.log(`Created document ${doc._id}`); +} +``` + +### For `beforeUpdate` Events +- **v1.1.0**: `ctx.new` = new values, `ctx.old` = previous values +- **v2.0.0**: `ctx.input.doc` = new values, `ctx.previous` = previous values +- Mutations should be applied to `ctx.input.doc` + +```typescript +// v1.1.0 +handler: async (ctx) => { + const newDoc = ctx.new; + const oldDoc = ctx.old; + if (newDoc.status !== oldDoc.status) { + ctx.new.status_changed_at = new Date().toISOString(); + } +} + +// v2.0.0 +handler: async (ctx: any) => { + const newDoc = ctx.input.doc as any; + const oldDoc = ctx.previous as any; + if (newDoc?.status !== oldDoc?.status) { + newDoc.status_changed_at = new Date().toISOString(); + } +} +``` + +### For `afterUpdate` Events +- **v1.1.0**: `ctx.new` = updated document, `ctx.old` = previous values +- **v2.0.0**: `ctx.result` = updated document, `ctx.previous` = previous values + +```typescript +// v1.1.0 +handler: async (ctx) => { + const newDoc = ctx.new; + const oldDoc = ctx.old; + if (newDoc.status === 'Closed' && oldDoc.status !== 'Closed') { + await sendNotification(newDoc); + } +} + +// v2.0.0 +handler: async (ctx: any) => { + const newDoc = ctx.result as any; + const oldDoc = ctx.previous as any; + if (newDoc?.status === 'Closed' && oldDoc?.status !== 'Closed') { + await sendNotification(newDoc); + } +} +``` + +### For `beforeDelete`/`afterDelete` Events +- **v1.1.0**: `ctx.old` contained the document being deleted +- **v2.0.0**: `ctx.previous` contains the document being deleted + +```typescript +// v1.1.0 +handler: async (ctx) => { + const doc = ctx.old; + console.log(`Deleting document ${doc._id}`); +} + +// v2.0.0 +handler: async (ctx: any) => { + const doc = ctx.previous as any; + console.log(`Deleting document ${doc._id}`); +} +``` + +### Handler Function Signature +- **v1.1.0**: `handler: async (ctx: HookContext) => { ... }` +- **v2.0.0**: `handler: async (ctx: any) => { ... }` (due to TypeScript strict typing) + +The handler function should now use `ctx: any` to avoid type conflicts with the Zod-inferred type. + +### Using `ctx.ql` (ObjectQL Engine) +- **v2.0.0**: `ctx.ql` is typed as `unknown`, must be cast to `any` when using + +```typescript +// v2.0.0 +handler: async (ctx: any) => { + const records = await (ctx.ql as any).find('account', { + filters: [['status', '=', 'Active']] + }); +} +``` + +## Testing Migration + +Test mocks must also be updated to match the new API: + +### Before (v1.1.0) +```typescript +const mockContext: HookContext = { + event: 'afterUpdate', + new: { id: '123', status: 'Active' }, + old: { id: '123', status: 'Pending' }, + ql: mockQl +}; +``` + +### After (v2.0.0) +```typescript +const mockContext = { + object: 'entity_name', + event: 'afterUpdate', + input: { doc: { id: '123', status: 'Active' } }, + result: { id: '123', status: 'Active' }, + previous: { id: '123', status: 'Pending' }, + ql: mockQl +}; +``` + +## Automated Migration Script + +For bulk migration, you can use the following pattern: + +1. Find all hook handlers: `grep -r "ctx\.new\|ctx\.old" packages/*/src/hooks/*.hook.ts` +2. Apply the transformations above based on the event type +3. Update handler signatures to use `ctx: any` +4. Add type assertions for `ctx.ql`, `ctx.result`, `ctx.previous`, and `ctx.input.doc` + +## Files Updated in This Migration + +### Hook Implementation Files (19 files) +- `packages/crm/src/hooks/`: account.hook.ts, activity.hook.ts, contact.hook.ts, lead.hook.ts, opportunity.hook.ts +- `packages/finance/src/hooks/`: contract.hook.ts, contract_renewal.hook.ts +- `packages/hr/src/hooks/`: candidate.hook.ts, employee.hook.ts, offer.hook.ts, performance_review.hook.ts +- `packages/marketing/src/hooks/`: campaign.hook.ts, campaign_member.hook.ts, roi.hook.ts +- `packages/products/src/hooks/`: pricebook.hook.ts, product.hook.ts, quote.hook.ts +- `packages/support/src/hooks/`: case.hook.ts, knowledge.hook.ts + +### Test Files (4 files) +- `packages/hr/__tests__/unit/hooks/`: candidate.hook.test.ts, employee.hook.test.ts, offer.hook.test.ts +- `packages/finance/__tests__/unit/hooks/`: contract_renewal.hook.test.ts + +## Verification + +After migration, verify that: +1. All packages build successfully: `pnpm -r build` +2. Validation passes: `pnpm validate` +3. All tests pass: `pnpm test` + +## Additional Resources + +- [@objectstack/spec v2.0.0 Release Notes](https://www.npmjs.com/package/@objectstack/spec) +- [ObjectStack Documentation](https://objectstack.ai) + +## Support + +If you encounter any issues during migration, please: +1. Check this migration guide +2. Review the test file updates as examples +3. Open an issue on the HotCRM repository diff --git a/package.json b/package.json index 6e5ecd834..e0db13a56 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "author": "HotCRM Team", "license": "MIT", "devDependencies": { - "@objectstack/cli": "^1.1.0", - "@objectstack/spec": "^1.1.0", + "@objectstack/cli": "^2.0.0", + "@objectstack/spec": "^2.0.0", "@types/node": "^25.2.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^6.14.0", diff --git a/packages/ai/package.json b/packages/ai/package.json index 857c51fa4..9904cfc10 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -23,13 +23,13 @@ "author": "HotCRM Team", "license": "MIT", "dependencies": { - "@objectstack/spec": "^1.1.0", + "@objectstack/spec": "^2.0.0", "axios": "^1.13.4", "typescript": "^5.3.0", "zod": "^4.3.6" }, "peerDependencies": { - "@objectstack/cli": "^1.1.0" + "@objectstack/cli": "^2.0.0" }, "devDependencies": { "@types/node": "^25.2.0", diff --git a/packages/core/package.json b/packages/core/package.json index 14df7fdb8..98395a298 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,7 +13,7 @@ "build": "tsc" }, "dependencies": { - "@objectstack/spec": "^1.1.0", + "@objectstack/spec": "^2.0.0", "zod": "^4.3.6" } } diff --git a/packages/crm/package.json b/packages/crm/package.json index c840d8422..401ba19ec 100644 --- a/packages/crm/package.json +++ b/packages/crm/package.json @@ -23,13 +23,13 @@ "license": "MIT", "dependencies": { "@hotcrm/ai": "workspace:*", - "@objectstack/spec": "^1.1.0", + "@objectstack/spec": "^2.0.0", "axios": "^1.13.4", "typescript": "^5.3.0", "zod": "^4.3.6" }, "peerDependencies": { - "@objectstack/cli": "^1.1.0" + "@objectstack/cli": "^2.0.0" }, "devDependencies": { "@types/node": "^25.2.0", diff --git a/packages/crm/src/hooks/account.hook.ts b/packages/crm/src/hooks/account.hook.ts index 1e4d0c15b..eb79fe6cb 100644 --- a/packages/crm/src/hooks/account.hook.ts +++ b/packages/crm/src/hooks/account.hook.ts @@ -1,13 +1,7 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; -// Types for Context -interface TriggerContext { - old?: Record; - new: Record; - db: typeof db; - user: { id: string; name: string; email: string; }; -} + /** * Account Health Score Calculation Trigger @@ -22,9 +16,9 @@ const AccountHealthScoreTrigger: Hook = { name: 'AccountHealthScoreTrigger', object: 'Account', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const account = ctx.new; + const account = ctx.input.doc as Record; // Calculate health score account.HealthScore = await calculateHealthScore(account, ctx); @@ -48,7 +42,7 @@ const AccountHealthScoreTrigger: Hook = { /** * Calculate account health score (0-100) */ -async function calculateHealthScore(account: Record, ctx: TriggerContext): Promise { +async function calculateHealthScore(account: Record, ctx: any): Promise { let score = 0; // Base score for active customer status (20 points) @@ -100,10 +94,10 @@ const AccountHierarchyTrigger: Hook = { name: 'AccountHierarchyTrigger', object: 'Account', events: ['afterInsert', 'afterUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const account = ctx.new; - const oldAccount = ctx.old; + const account = ctx.result as Record; + const oldAccount = ctx.previous as Record | undefined; // Check if parent changed const parentChanged = oldAccount && (oldAccount.ParentId !== account.ParentId); @@ -132,7 +126,7 @@ const AccountHierarchyTrigger: Hook = { /** * Update parent account metrics by aggregating child account data */ -async function updateParentAccountMetrics(parentId: string, ctx: TriggerContext): Promise { +async function updateParentAccountMetrics(parentId: string, ctx: any): Promise { console.log(`๐Ÿ”„ Updating parent account metrics: ${parentId}`); // In real implementation, would query all child accounts and aggregate @@ -153,7 +147,7 @@ async function updateParentAccountMetrics(parentId: string, ctx: TriggerContext) /** * Cascade ownership changes to child accounts */ -async function cascadeOwnershipChange(accountId: string, newOwnerId: string, ctx: TriggerContext): Promise { +async function cascadeOwnershipChange(accountId: string, newOwnerId: string, ctx: any): Promise { console.log(`๐Ÿ”„ Cascading ownership change to child accounts of ${accountId}`); // In real implementation, would query and update all child accounts @@ -180,9 +174,9 @@ const AccountStatusAutomationTrigger: Hook = { name: 'AccountStatusAutomationTrigger', object: 'Account', events: ['beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const account = ctx.new; + const account = ctx.input.doc as Record; // Auto-upgrade to Active Customer if has contract value if (account.CustomerStatus === 'Prospect' && account.ContractValue > 0) { diff --git a/packages/crm/src/hooks/activity.hook.ts b/packages/crm/src/hooks/activity.hook.ts index d6dd78f7a..a386ba245 100644 --- a/packages/crm/src/hooks/activity.hook.ts +++ b/packages/crm/src/hooks/activity.hook.ts @@ -1,13 +1,7 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; -// Types for Context -interface TriggerContext { - old?: Record; - new: Record; - db: typeof db; - user: { id: string; name: string; email: string; }; -} + /** * Activity Auto-Complete Trigger @@ -52,10 +46,10 @@ const ActivityRelatedObjectUpdatesTrigger: Hook = { name: 'ActivityRelatedObjectUpdatesTrigger', object: 'Activity', events: ['afterInsert', 'afterUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const activity = ctx.new; - const oldActivity = ctx.old; + const activity = ctx.result as Record; + const oldActivity = ctx.previous as Record | undefined; // Only process if activity is completed or date changed const isCompleted = activity.Status === 'Completed'; @@ -88,7 +82,7 @@ const ActivityRelatedObjectUpdatesTrigger: Hook = { /** * Update Contact's last activity date */ -async function updateContactLastActivityDate(whoId: string, activityDate: string, ctx: TriggerContext): Promise { +async function updateContactLastActivityDate(whoId: string, activityDate: string, ctx: any): Promise { console.log(`๐Ÿ”„ Updating LastContactDate for contact: ${whoId}`); // In real implementation: @@ -105,7 +99,7 @@ async function updateContactLastActivityDate(whoId: string, activityDate: string /** * Update related object's last activity date */ -async function updateWhatObjectLastActivityDate(whatId: string, activityDate: string, ctx: TriggerContext): Promise { +async function updateWhatObjectLastActivityDate(whatId: string, activityDate: string, ctx: any): Promise { console.log(`๐Ÿ”„ Updating LastActivityDate for related object: ${whatId}`); // In real implementation, would need to determine object type from WhatId @@ -135,10 +129,10 @@ const ActivityCompletionTrigger: Hook = { name: 'ActivityCompletionTrigger', object: 'Activity', events: ['beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const activity = ctx.new; - const oldActivity = ctx.old; + const activity = ctx.input.doc as Record; + const oldActivity = ctx.previous as Record | undefined; // Check if status changed to Completed if (oldActivity && oldActivity.Status !== 'Completed' && activity.Status === 'Completed') { @@ -161,7 +155,7 @@ const ActivityCompletionTrigger: Hook = { /** * Create next recurrence of a recurring activity */ -async function createNextRecurrence(activity: Record, ctx: TriggerContext): Promise { +async function createNextRecurrence(activity: Record, ctx: any): Promise { console.log(`๐Ÿ”„ Creating next recurrence for: ${activity.Subject}`); // Calculate next occurrence date based on pattern @@ -294,9 +288,9 @@ const ActivityTypeValidationTrigger: Hook = { name: 'ActivityTypeValidationTrigger', object: 'Activity', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const activity = ctx.new; + const activity = ctx.input.doc as Record; // Log activity type for tracking console.log(`๐Ÿ“ Activity: ${activity.Type} - ${activity.Subject}`); diff --git a/packages/crm/src/hooks/contact.hook.ts b/packages/crm/src/hooks/contact.hook.ts index cd7b852cc..8ece6c3f5 100644 --- a/packages/crm/src/hooks/contact.hook.ts +++ b/packages/crm/src/hooks/contact.hook.ts @@ -1,13 +1,7 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; -// Types for Context -interface TriggerContext { - old?: Record; - new: Record; - db: typeof db; - user: { id: string; name: string; email: string; }; -} + /** * Contact Last Contact Date Tracking Trigger @@ -42,9 +36,9 @@ const ContactDecisionChainTrigger: Hook = { name: 'ContactDecisionChainTrigger', object: 'Contact', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const contact = ctx.new; + const contact = ctx.input.doc as Record; // Auto-set InfluenceLevel based on Level if (!contact.InfluenceLevel && contact.Level) { @@ -93,9 +87,9 @@ const ContactDecisionMakerValidationTrigger: Hook = { name: 'ContactDecisionMakerValidationTrigger', object: 'Contact', events: ['afterInsert', 'afterUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const contact = ctx.new; + const contact = ctx.result as Record; if (!contact.AccountId) { return; @@ -134,10 +128,10 @@ const ContactDuplicateDetectionTrigger: Hook = { name: 'ContactDuplicateDetectionTrigger', object: 'Contact', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const contact = ctx.new; - const isNew = !ctx.old; + const contact = ctx.input.doc as Record; + const isNew = !ctx.previous; // Check for potential duplicates by name if (contact.FirstName && contact.LastName && contact.AccountId) { @@ -182,9 +176,9 @@ const ContactRelationshipStrengthTrigger: Hook = { name: 'ContactRelationshipStrengthTrigger', object: 'Contact', events: ['beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const contact = ctx.new; + const contact = ctx.input.doc as Record; // Only update if LastContactDate changed or this is a periodic update if (!contact.LastContactDate) { diff --git a/packages/crm/src/hooks/lead.hook.ts b/packages/crm/src/hooks/lead.hook.ts index 326eb1030..393246db4 100644 --- a/packages/crm/src/hooks/lead.hook.ts +++ b/packages/crm/src/hooks/lead.hook.ts @@ -182,7 +182,7 @@ async function calculateLeadScore(lead: Lead, ctx: HookContext): Promise */ async function runAssignmentRules(lead: Lead, ctx: HookContext): Promise { try { - const rules: any[] = await ctx.ql.find('assignment_rule', { + const rules: any[] = await (ctx.ql as any).find('assignment_rule', { filters: [ ['object_name', '=', 'lead'], ['active', '=', true] @@ -317,7 +317,7 @@ const LeadStatusChangeTrigger: Hook = { // Log activity try { - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `็บฟ็ดขๅทฒ่ฝฌๅŒ–: ${lead.FirstName} ${lead.LastName}`, Type: 'Conversion', Status: 'Completed', @@ -342,7 +342,7 @@ async function handleLeadConversion(ctx: HookContext): Promise { // Log activity for conversion try { - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `็บฟ็ดข่ฝฌๆข: ${lead.FirstName} ${lead.LastName}`, Type: 'Conversion', Status: 'Completed', @@ -372,7 +372,7 @@ async function handleLeadUnqualification(ctx: HookContext): Promise { // Log activity try { - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `็บฟ็ดขไธๅˆๆ ผ: ${lead.FirstName} ${lead.LastName}`, Type: 'Disqualification', Status: 'Completed', @@ -395,7 +395,7 @@ async function logStatusChange(ctx: HookContext): Promise { const lead = ctx.input; const oldStatus = ctx.previous?.Status || 'Unknown'; - await ctx.ql.doc.create('activity', { + await (ctx.ql as any).doc.create('activity', { Subject: `็บฟ็ดข็Šถๆ€ๅ˜ๆ›ด: ${oldStatus} โ†’ ${ctx.input.Status}`, Type: 'Status Change', Status: 'Completed', diff --git a/packages/crm/src/hooks/opportunity.hook.ts b/packages/crm/src/hooks/opportunity.hook.ts index a31bd65e6..3965d0a8f 100644 --- a/packages/crm/src/hooks/opportunity.hook.ts +++ b/packages/crm/src/hooks/opportunity.hook.ts @@ -1,20 +1,14 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; -// Types for Context embedded here or imported if available -export interface TriggerContext { - old?: Record; - new: Record; - db: typeof db; - user: { id: string; name: string; email: string; }; -} + const OpportunityValidation: Hook = { name: 'OpportunityValidation', object: 'Opportunity', events: ['beforeUpdate', 'beforeInsert'], - handler: async (ctx: TriggerContext) => { - const opp = ctx.new; + handler: async (ctx: HookContext) => { + const opp = ctx.input.doc as Record; // 1. Validate "closed_won" if (opp.stage === 'closed_won') { @@ -25,7 +19,8 @@ const OpportunityValidation: Hook = { // 2. Validate "Proposal" - Must have a Quote // Note: We only check this on Update to avoid issues during initial import - if (opp.stage === 'Proposal' && ctx.old && ctx.old.stage !== 'Proposal') { + const ctxPrevious = ctx.previous as Record | undefined; + if (opp.stage === 'Proposal' && ctxPrevious && ctxPrevious.stage !== 'Proposal') { const quoteCount = await countRelatedQuotes(ctx, opp._id); if (quoteCount === 0) { throw new Error('Validation Error: Cannot move to Proposal stage without an active Quote. Please create a Quote first.'); @@ -46,21 +41,21 @@ const OpportunityStageChange: Hook = { name: 'OpportunityStageChange', object: 'Opportunity', events: ['afterUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { // Defensive check - if (!ctx.old || !ctx.new) { - console.warn('โš ๏ธ Trigger called without old/new context'); + if (!ctx.previous || !ctx.result) { + console.warn('โš ๏ธ Trigger called without previous/result context'); return; } // Check if Stage actually changed - const stageChanged = ctx.old.Stage !== ctx.new.Stage; + const stageChanged = (ctx.previous as Record | undefined)?.Stage !== (ctx.result as Record)?.Stage; if (!stageChanged) { return; } - console.log(`๐Ÿ”„ Stage changed from "${ctx.old.Stage}" to "${ctx.new.Stage}"`); + console.log(`๐Ÿ”„ Stage changed from "${(ctx.previous as Record | undefined)?.Stage}" to "${(ctx.result as Record)?.Stage}"`); // Log activity for stage change await logStageChange(ctx); @@ -69,12 +64,12 @@ const OpportunityStageChange: Hook = { await validateStageRequirements(ctx); // Handle "closed_won" scenario - if (ctx.new.Stage === 'closed_won') { + if ((ctx.result as Record)?.Stage === 'closed_won') { await handleClosedWon(ctx); } // Handle "closed_lost" scenario - if (ctx.new.Stage === 'closed_lost') { + if ((ctx.result as Record)?.Stage === 'closed_lost') { await handleClosedLost(ctx); } @@ -88,9 +83,9 @@ const OpportunityStageChange: Hook = { /** * Handle Closed Won automation */ -async function handleClosedWon(ctx: TriggerContext): Promise { +async function handleClosedWon(ctx: any): Promise { console.log('โœ… Processing Closed Won automation...'); - const opportunity = ctx.new; + const opportunity = ctx.result; if (!opportunity.AccountId) { console.error('โŒ Cannot process: Opportunity has no AccountId'); @@ -167,9 +162,9 @@ async function handleClosedWon(ctx: TriggerContext): Promise { } } -async function handleClosedLost(ctx: TriggerContext): Promise { +async function handleClosedLost(ctx: any): Promise { console.log('โŒ Processing Closed Lost automation...'); - const opportunity = ctx.new; + const opportunity = ctx.result; if (!opportunity.AccountId) { return; @@ -200,12 +195,12 @@ async function handleClosedLost(ctx: TriggerContext): Promise { /** * Log activity when stage changes */ -async function logStageChange(ctx: TriggerContext): Promise { +async function logStageChange(ctx: any): Promise { try { - const opportunity = ctx.new; - const oldStage = ctx.old?.Stage || 'Unknown'; + const opportunity = ctx.result; + const oldStage = ctx.previous?.Stage || 'Unknown'; await ctx.db.doc.create('Activity', { - Subject: `ๅ•†ๆœบ้˜ถๆฎตๅ˜ๆ›ด: ${oldStage} โ†’ ${ctx.new.Stage}`, + Subject: `ๅ•†ๆœบ้˜ถๆฎตๅ˜ๆ›ด: ${oldStage} โ†’ ${ctx.result.Stage}`, Type: 'Stage Change', Status: 'Completed', Priority: 'Normal', @@ -213,7 +208,7 @@ async function logStageChange(ctx: TriggerContext): Promise { WhatId: opportunity.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `ๅ•†ๆœบ้˜ถๆฎตไปŽ "${oldStage}" ๅ˜ๆ›ดไธบ "${ctx.new.Stage}"` + Description: `ๅ•†ๆœบ้˜ถๆฎตไปŽ "${oldStage}" ๅ˜ๆ›ดไธบ "${ctx.result.Stage}"` }); } catch (error) { console.error('โŒ Failed to log stage change activity:', error); @@ -223,8 +218,8 @@ async function logStageChange(ctx: TriggerContext): Promise { /** * Validate required fields for advanced stages */ -async function validateStageRequirements(ctx: TriggerContext): Promise { - const opportunity = ctx.new; +async function validateStageRequirements(ctx: any): Promise { + const opportunity = ctx.result; const stage = opportunity.Stage; const warnings: string[] = []; @@ -260,7 +255,7 @@ async function validateStageRequirements(ctx: TriggerContext): Promise { /** * Helper: Count related quotes */ -async function countRelatedQuotes(ctx: TriggerContext, opportunityId: string): Promise { +async function countRelatedQuotes(ctx: any, opportunityId: string): Promise { // Check if quote object exists first (it's in products package) try { // In a real monorepo with strict boundaries, we might use a decoupled service. diff --git a/packages/finance/__tests__/unit/hooks/contract_renewal.hook.test.ts b/packages/finance/__tests__/unit/hooks/contract_renewal.hook.test.ts index 1b5d1e745..b93c6a7cb 100644 --- a/packages/finance/__tests__/unit/hooks/contract_renewal.hook.test.ts +++ b/packages/finance/__tests__/unit/hooks/contract_renewal.hook.test.ts @@ -22,23 +22,25 @@ describe('ContractRenewalCheck', () => { }); it('should create renewal opportunity when contract is activated and expires within 90 days', async () => { + const newDoc = { + _id: 'contract_1', + status: 'Activated', + end_date: daysFromNow(60), + contract_number: 'CNT-001', + account: 'acc_1', + contract_value: 50000 + }; + const oldDoc = { + _id: 'contract_1', + status: 'Draft', + end_date: daysFromNow(60), + contract_number: 'CNT-001', + account: 'acc_1', + contract_value: 50000 + }; const ctx = { - new: { - _id: 'contract_1', - status: 'Activated', - end_date: daysFromNow(60), - contract_number: 'CNT-001', - account: 'acc_1', - contract_value: 50000 - }, - old: { - _id: 'contract_1', - status: 'Draft', - end_date: daysFromNow(60), - contract_number: 'CNT-001', - account: 'acc_1', - contract_value: 50000 - } + result: newDoc, + previous: oldDoc }; // No existing renewal opportunity @@ -46,7 +48,7 @@ describe('ContractRenewalCheck', () => { // Insert opportunity returns new record (db.insert as Mock).mockResolvedValueOnce({ _id: 'opp_1', - name: `Renewal: ${ctx.new.contract_number}` + name: `Renewal: ${newDoc.contract_number}` }); // Insert task (db.insert as Mock).mockResolvedValueOnce({ _id: 'task_1' }); @@ -85,7 +87,7 @@ describe('ContractRenewalCheck', () => { it('should not create opportunity when contract expires after 90 days', async () => { const ctx = { - new: { + result: { _id: 'contract_2', status: 'Activated', end_date: daysFromNow(120), @@ -93,7 +95,7 @@ describe('ContractRenewalCheck', () => { account: 'acc_2', contract_value: 30000 }, - old: { + previous: { _id: 'contract_2', status: 'Draft', end_date: daysFromNow(120), @@ -111,7 +113,7 @@ describe('ContractRenewalCheck', () => { it('should not trigger when status does not change to Activated', async () => { const ctx = { - new: { + result: { _id: 'contract_3', status: 'Draft', end_date: daysFromNow(30), @@ -119,7 +121,7 @@ describe('ContractRenewalCheck', () => { account: 'acc_3', contract_value: 10000 }, - old: { + previous: { _id: 'contract_3', status: 'Draft', end_date: daysFromNow(30), @@ -137,7 +139,7 @@ describe('ContractRenewalCheck', () => { it('should not create a duplicate opportunity if one already exists', async () => { const ctx = { - new: { + result: { _id: 'contract_4', status: 'Activated', end_date: daysFromNow(45), @@ -145,7 +147,7 @@ describe('ContractRenewalCheck', () => { account: 'acc_4', contract_value: 80000 }, - old: { + previous: { _id: 'contract_4', status: 'Draft', end_date: daysFromNow(45), @@ -165,7 +167,7 @@ describe('ContractRenewalCheck', () => { it('should handle errors gracefully', async () => { const ctx = { - new: { + result: { _id: 'contract_5', status: 'Activated', end_date: daysFromNow(30), @@ -173,7 +175,7 @@ describe('ContractRenewalCheck', () => { account: 'acc_5', contract_value: 20000 }, - old: { + previous: { _id: 'contract_5', status: 'Draft', end_date: daysFromNow(30), @@ -199,7 +201,7 @@ describe('ContractRenewalCheck', () => { it('should set task priority to High when contract expires within 30 days', async () => { const ctx = { - new: { + result: { _id: 'contract_6', status: 'Activated', end_date: daysFromNow(20), @@ -207,7 +209,7 @@ describe('ContractRenewalCheck', () => { account: 'acc_6', contract_value: 60000 }, - old: { + previous: { _id: 'contract_6', status: 'Draft', end_date: daysFromNow(20), @@ -236,7 +238,7 @@ describe('ContractExpirationAlert', () => { it('should send alert when contract expires within 30 days', async () => { const ctx = { - new: { + result: { _id: 'contract_10', status: 'Activated', end_date: daysFromNow(15), @@ -245,7 +247,7 @@ describe('ContractExpirationAlert', () => { contract_value: 100000, renewal_reminder_sent: false }, - old: { + previous: { _id: 'contract_10', status: 'Draft', end_date: daysFromNow(15), @@ -278,7 +280,7 @@ describe('ContractExpirationAlert', () => { it('should not alert when contract expires after 30 days', async () => { const ctx = { - new: { + result: { _id: 'contract_11', status: 'Activated', end_date: daysFromNow(60), @@ -287,7 +289,7 @@ describe('ContractExpirationAlert', () => { contract_value: 50000, renewal_reminder_sent: false }, - old: { + previous: { _id: 'contract_11', status: 'Draft', end_date: daysFromNow(60), @@ -306,7 +308,7 @@ describe('ContractExpirationAlert', () => { it('should not alert when renewal_reminder_sent is already true', async () => { const ctx = { - new: { + result: { _id: 'contract_12', status: 'Activated', end_date: daysFromNow(10), @@ -315,7 +317,7 @@ describe('ContractExpirationAlert', () => { contract_value: 70000, renewal_reminder_sent: true }, - old: { + previous: { _id: 'contract_12', status: 'Draft', end_date: daysFromNow(10), @@ -334,7 +336,7 @@ describe('ContractExpirationAlert', () => { it('should not alert when status is not Activated', async () => { const ctx = { - new: { + result: { _id: 'contract_13', status: 'Draft', end_date: daysFromNow(10), @@ -343,7 +345,7 @@ describe('ContractExpirationAlert', () => { contract_value: 25000, renewal_reminder_sent: false }, - old: { + previous: { _id: 'contract_13', status: 'Draft', end_date: daysFromNow(20), @@ -362,7 +364,7 @@ describe('ContractExpirationAlert', () => { it('should handle errors gracefully', async () => { const ctx = { - new: { + result: { _id: 'contract_14', status: 'Activated', end_date: daysFromNow(5), @@ -371,7 +373,7 @@ describe('ContractExpirationAlert', () => { contract_value: 30000, renewal_reminder_sent: false }, - old: { + previous: { _id: 'contract_14', status: 'Draft', end_date: daysFromNow(5), diff --git a/packages/finance/package.json b/packages/finance/package.json index 0229dc310..084715a43 100644 --- a/packages/finance/package.json +++ b/packages/finance/package.json @@ -21,11 +21,11 @@ "author": "HotCRM Team", "license": "MIT", "dependencies": { - "@objectstack/spec": "^1.1.0", + "@objectstack/spec": "^2.0.0", "typescript": "^5.3.0" }, "peerDependencies": { - "@objectstack/cli": "^1.1.0" + "@objectstack/cli": "^2.0.0" }, "devDependencies": { "@types/node": "^25.2.0", diff --git a/packages/finance/src/hooks/contract.hook.ts b/packages/finance/src/hooks/contract.hook.ts index 4760b5f66..6d339d08f 100644 --- a/packages/finance/src/hooks/contract.hook.ts +++ b/packages/finance/src/hooks/contract.hook.ts @@ -1,16 +1,16 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; const ContractBillingHook: Hook = { name: 'ContractBillingAutomation', object: 'contract', events: ['afterUpdate'], - handler: async (ctx) => { - const newDoc = ctx.new; - const oldDoc = ctx.old; + handler: async (ctx: HookContext) => { + const newDoc = ctx.result as Record; + const oldDoc = ctx.previous as Record | undefined; // Trigger only when status changes to 'Activated' - if (newDoc.status === 'Activated' && oldDoc.status !== 'Activated') { + if (newDoc.status === 'Activated' && oldDoc?.status !== 'Activated') { console.log(`Starting Billing Process for Contract ${newDoc.contract_number}`); try { diff --git a/packages/finance/src/hooks/contract_renewal.hook.ts b/packages/finance/src/hooks/contract_renewal.hook.ts index 883c24d94..a4524bdb4 100644 --- a/packages/finance/src/hooks/contract_renewal.hook.ts +++ b/packages/finance/src/hooks/contract_renewal.hook.ts @@ -1,4 +1,4 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; const RENEWAL_WINDOW_DAYS = 90; @@ -8,12 +8,12 @@ const ContractRenewalCheck: Hook = { name: 'ContractRenewalCheck', object: 'contract', events: ['afterUpdate'], - handler: async (ctx) => { - const newDoc = ctx.new; - const oldDoc = ctx.old; + handler: async (ctx: HookContext) => { + const newDoc = ctx.result as Record; + const oldDoc = ctx.previous as Record | undefined; - const isActivated = newDoc.status === 'Activated' && oldDoc.status !== 'Activated'; - const endDateChanged = newDoc.end_date !== oldDoc.end_date && newDoc.status === 'Activated'; + const isActivated = newDoc.status === 'Activated' && oldDoc?.status !== 'Activated'; + const endDateChanged = newDoc.end_date !== oldDoc?.end_date && newDoc.status === 'Activated'; if (!isActivated && !endDateChanged) return; if (!newDoc.end_date) return; @@ -79,16 +79,16 @@ const ContractExpirationAlert: Hook = { name: 'ContractExpirationAlert', object: 'contract', events: ['afterUpdate'], - handler: async (ctx) => { - const newDoc = ctx.new; - const oldDoc = ctx.old; + handler: async (ctx: HookContext) => { + const newDoc = ctx.result as Record; + const oldDoc = ctx.previous as Record | undefined; if (newDoc.status !== 'Activated') return; if (newDoc.renewal_reminder_sent) return; if (!newDoc.end_date) return; - const endDateChanged = newDoc.end_date !== oldDoc.end_date; - const statusChanged = newDoc.status !== oldDoc.status; + const endDateChanged = newDoc.end_date !== oldDoc?.end_date; + const statusChanged = newDoc.status !== oldDoc?.status; if (!endDateChanged && !statusChanged) return; try { diff --git a/packages/hr/__tests__/unit/hooks/candidate.hook.test.ts b/packages/hr/__tests__/unit/hooks/candidate.hook.test.ts index fd0eb168d..ec9c608a3 100644 --- a/packages/hr/__tests__/unit/hooks/candidate.hook.test.ts +++ b/packages/hr/__tests__/unit/hooks/candidate.hook.test.ts @@ -14,24 +14,41 @@ const mockQlDocUpdate = vi.fn(); const createMockContext = ( event: 'beforeInsert' | 'beforeUpdate' | 'afterUpdate', input: any, - previous?: any -): HookContext => ({ - event, - input, - previous, - ql: { - find: mockQlFind, - doc: { - get: mockQlDocGet, - create: mockQlDocCreate, - update: mockQlDocUpdate - } - } as any, - session: { - userId: 'user_123', - tenantId: 'tenant_123' - } as any -}); + previous?: any, + result?: any +): HookContext => { + const baseContext = { + event, + ql: { + find: mockQlFind, + doc: { + get: mockQlDocGet, + create: mockQlDocCreate, + update: mockQlDocUpdate + } + } as any, + session: { + userId: 'user_123', + tenantId: 'tenant_123' + } as any + }; + + if (event === 'beforeInsert' || event === 'beforeUpdate') { + return { + ...baseContext, + input: { doc: input }, + previous + }; + } else if (event === 'afterUpdate') { + return { + ...baseContext, + input: { doc: input }, + result: result || input, + previous + }; + } + return baseContext; +}; describe('Candidate Hook - CandidateScoringTrigger', () => { beforeEach(() => { @@ -65,10 +82,10 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.score).toBeDefined(); - expect(candidate.score).toBeGreaterThan(0); - expect(candidate.score).toBeLessThanOrEqual(100); - expect(candidate.score).toBeGreaterThan(70); // Should be high score for complete profile + expect(ctx.input.doc.score).toBeDefined(); + expect(ctx.input.doc.score).toBeGreaterThan(0); + expect(ctx.input.doc.score).toBeLessThanOrEqual(100); + expect(ctx.input.doc.score).toBeGreaterThan(70); // Should be high score for complete profile }); it('should assign lower score for incomplete profile', async () => { @@ -90,8 +107,8 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.score).toBeDefined(); - expect(candidate.score).toBeLessThan(50); // Should be lower for incomplete profile + expect(ctx.input.doc.score).toBeDefined(); + expect(ctx.input.doc.score).toBeLessThan(50); // Should be lower for incomplete profile }); it('should give maximum education points for PhD', async () => { @@ -114,7 +131,7 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.score).toBeGreaterThan(60); // PhD + 15 years should score high + expect(ctx.input.doc.score).toBeGreaterThan(60); // PhD + 15 years should score high }); it('should award maximum experience points for 10+ years', async () => { @@ -137,7 +154,7 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.score).toBeGreaterThan(50); + expect(ctx.input.doc.score).toBeGreaterThan(50); }); }); @@ -257,7 +274,7 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.status).toBe('under_review'); + expect(ctx.input.doc.status).toBe('under_review'); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('passed auto-screening') ); @@ -284,7 +301,7 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.status).toBe('new'); + expect(ctx.input.doc.status).toBe('new'); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('did not pass auto-screening') ); @@ -310,7 +327,7 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.status).toBe('new'); + expect(ctx.input.doc.status).toBe('new'); }); it('should fail auto-screening for candidate without resume', async () => { @@ -331,7 +348,7 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.status).toBe('new'); + expect(ctx.input.doc.status).toBe('new'); }); it('should not auto-screen if status is not New', async () => { @@ -353,7 +370,7 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.status).toBe('Interviewing'); // Should remain unchanged + expect(ctx.input.doc.status).toBe('Interviewing'); // Should remain unchanged }); }); @@ -388,7 +405,7 @@ describe('Candidate Hook - CandidateScoringTrigger', () => { await CandidateScoringTrigger.handler(ctx); // Assert - expect(candidate.score).toBeGreaterThan(previous.score); + expect(ctx.input.doc.score).toBeGreaterThan(previous.score); }); }); diff --git a/packages/hr/__tests__/unit/hooks/employee.hook.test.ts b/packages/hr/__tests__/unit/hooks/employee.hook.test.ts index 6e19d209a..3469a53c6 100644 --- a/packages/hr/__tests__/unit/hooks/employee.hook.test.ts +++ b/packages/hr/__tests__/unit/hooks/employee.hook.test.ts @@ -15,24 +15,41 @@ const mockQlDocUpdate = vi.fn(); const createMockContext = ( event: 'beforeInsert' | 'beforeUpdate' | 'afterInsert' | 'afterUpdate', input: any, - previous?: any -): HookContext => ({ - event, - input, - previous, - ql: { - find: mockQlFind, - doc: { - get: mockQlDocGet, - create: mockQlDocCreate, - update: mockQlDocUpdate - } - } as any, - session: { - userId: 'user_123', - tenantId: 'tenant_123' - } as any -}); + previous?: any, + result?: any +): HookContext => { + const baseContext = { + event, + ql: { + find: mockQlFind, + doc: { + get: mockQlDocGet, + create: mockQlDocCreate, + update: mockQlDocUpdate + } + } as any, + session: { + userId: 'user_123', + tenantId: 'tenant_123' + } as any + }; + + if (event === 'beforeInsert' || event === 'beforeUpdate') { + return { + ...baseContext, + input: { doc: input }, + previous + }; + } else if (event === 'afterInsert' || event === 'afterUpdate') { + return { + ...baseContext, + input: { doc: input }, + result: result || input, + previous + }; + } + return baseContext; +}; describe('Employee Hook - EmployeeDataValidationTrigger', () => { beforeEach(() => { @@ -55,7 +72,7 @@ describe('Employee Hook - EmployeeDataValidationTrigger', () => { await EmployeeDataValidationTrigger.handler(ctx); // Assert - expect(employee.full_name).toBe('ๆŽๆ˜Ž'); // LastFirst format + expect(ctx.input.doc.full_name).toBe('ๆŽๆ˜Ž'); // LastFirst format }); it('should not override existing full name', async () => { @@ -74,7 +91,7 @@ describe('Employee Hook - EmployeeDataValidationTrigger', () => { await EmployeeDataValidationTrigger.handler(ctx); // Assert - expect(employee.full_name).toBe('Li Ming'); + expect(ctx.input.doc.full_name).toBe('Li Ming'); }); it('should allow future hire dates for pre-boarding', async () => { diff --git a/packages/hr/__tests__/unit/hooks/offer.hook.test.ts b/packages/hr/__tests__/unit/hooks/offer.hook.test.ts index 85f180e13..c68628354 100644 --- a/packages/hr/__tests__/unit/hooks/offer.hook.test.ts +++ b/packages/hr/__tests__/unit/hooks/offer.hook.test.ts @@ -15,24 +15,43 @@ const mockQlDocUpdate = vi.fn(); const createMockContext = ( event: 'beforeInsert' | 'beforeUpdate' | 'afterUpdate', input: any, - previous?: any -): HookContext => ({ - event, - input, - previous, - ql: { - find: mockQlFind, - doc: { - get: mockQlDocGet, - create: mockQlDocCreate, - update: mockQlDocUpdate - } - } as any, - session: { - userId: 'user_123', - tenantId: 'tenant_123' - } as any -}); + previous?: any, + result?: any +): any => { + const baseContext = { + object: 'offer', + event, + ql: { + find: mockQlFind, + doc: { + get: mockQlDocGet, + create: mockQlDocCreate, + update: mockQlDocUpdate + } + } as any, + session: { + userId: 'user_123', + tenantId: 'tenant_123' + } as any + }; + + if (event === 'beforeInsert' || event === 'beforeUpdate') { + return { + ...baseContext, + input: { doc: input }, + previous + }; + } else if (event === 'afterUpdate') { + return { + ...baseContext, + input: { doc: input }, + result: result || input, + previous + }; + } + + return baseContext; +}; describe('Offer Hook - OfferCreationTrigger', () => { beforeEach(() => { @@ -58,8 +77,8 @@ describe('Offer Hook - OfferCreationTrigger', () => { await OfferCreationTrigger.handler(ctx); // Assert - expect(offer.offer_number).toBeDefined(); - expect(offer.offer_number).toMatch(/^OFF-\d{6}-\d{4}$/); + expect(ctx.input.doc.offer_number).toBeDefined(); + expect(ctx.input.doc.offer_number).toMatch(/^OFF-\d{6}-\d{4}$/); }); it('should not override existing offer number', async () => { @@ -76,7 +95,7 @@ describe('Offer Hook - OfferCreationTrigger', () => { await OfferCreationTrigger.handler(ctx); // Assert - expect(offer.offer_number).toBe('CUSTOM-001'); + expect(ctx.input.doc.offer_number).toBe('CUSTOM-001'); }); it('should generate sequential offer numbers', async () => { @@ -103,7 +122,7 @@ describe('Offer Hook - OfferCreationTrigger', () => { await OfferCreationTrigger.handler(ctx); // Assert - expect(offer1.offer_number).toBe(`OFF-${yearMonth}-0006`); + expect(ctx.input.doc.offer_number).toBe(`OFF-${yearMonth}-0006`); }); }); @@ -123,7 +142,7 @@ describe('Offer Hook - OfferCreationTrigger', () => { await OfferCreationTrigger.handler(ctx); // Assert - expect(offer.expiry_date).toBe('2024-06-08'); // 7 days after 2024-06-01 + expect(ctx.input.doc.expiry_date).toBe('2024-06-08'); // 7 days after 2024-06-01 }); it('should not override existing expiry date', async () => { @@ -142,7 +161,7 @@ describe('Offer Hook - OfferCreationTrigger', () => { await OfferCreationTrigger.handler(ctx); // Assert - expect(offer.expiry_date).toBe('2024-06-15'); + expect(ctx.input.doc.expiry_date).toBe('2024-06-15'); }); }); @@ -162,7 +181,7 @@ describe('Offer Hook - OfferCreationTrigger', () => { await OfferCreationTrigger.handler(ctx); // Assert - expect(offer.status).toBe('Draft'); + expect(ctx.input.doc.status).toBe('Draft'); }); it('should not override existing status', async () => { @@ -181,7 +200,7 @@ describe('Offer Hook - OfferCreationTrigger', () => { await OfferCreationTrigger.handler(ctx); // Assert - expect(offer.status).toBe('Approved'); + expect(ctx.input.doc.status).toBe('Approved'); }); }); @@ -258,9 +277,9 @@ describe('Offer Hook - OfferApprovalTrigger', () => { await OfferApprovalTrigger.handler(ctx); // Assert - expect(offer.status).toBe('Approved'); - expect(offer.approved_date).toBeDefined(); - expect(offer.approved_by).toBe('user_123'); + expect(ctx.input.doc.status).toBe('Approved'); + expect(ctx.input.doc.approved_date).toBeDefined(); + expect(ctx.input.doc.approved_by).toBe('user_123'); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('approved by user_123') ); @@ -289,7 +308,7 @@ describe('Offer Hook - OfferApprovalTrigger', () => { await OfferApprovalTrigger.handler(ctx); // Assert - expect(offer.status).toBe('Sent'); // Should remain Sent + expect(ctx.input.doc.status).toBe('Sent'); // Should remain Sent }); it('should handle offer rejection', async () => { @@ -314,7 +333,7 @@ describe('Offer Hook - OfferApprovalTrigger', () => { await OfferApprovalTrigger.handler(ctx); // Assert - expect(offer.status).toBe('Draft'); + expect(ctx.input.doc.status).toBe('Draft'); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('rejected in approval') ); @@ -345,7 +364,7 @@ describe('Offer Hook - OfferApprovalTrigger', () => { // Assert // Should not update approved_date since it was already approved - expect(offer.approved_date).toBe('2024-05-15'); + expect(ctx.input.doc.approved_date).toBe('2024-05-15'); }); }); @@ -367,7 +386,7 @@ describe('Offer Hook - OfferApprovalTrigger', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(); // Force an error by making offer have no status - delete offer.status; + delete ctx.input.doc.status; // Act & Assert await expect(OfferApprovalTrigger.handler(ctx)).resolves.not.toThrow(); @@ -395,7 +414,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { status: 'Draft' }; - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(); // Act @@ -422,7 +441,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { status: 'Draft' }; - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(); // Act @@ -455,7 +474,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocUpdate.mockResolvedValue({}); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(); // Act @@ -492,7 +511,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocUpdate.mockResolvedValue({}); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); // Act await OfferStatusChangeTrigger.handler(ctx); @@ -532,7 +551,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocGet.mockResolvedValue(mockCandidate); mockQlDocCreate.mockResolvedValue({ id: 'emp_123' }); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(); // Act @@ -578,7 +597,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocGet.mockResolvedValue(mockCandidate); mockQlDocCreate.mockResolvedValue({ id: 'emp_123' }); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); // Act await OfferStatusChangeTrigger.handler(ctx); @@ -625,7 +644,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocCreate.mockResolvedValue({ id: 'emp_123' }); mockQlFind.mockResolvedValue([]); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(); // Act @@ -680,7 +699,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocCreate.mockResolvedValue({ id: 'emp_123' }); mockQlFind.mockResolvedValue([]); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); // Act await OfferStatusChangeTrigger.handler(ctx); @@ -710,7 +729,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocUpdate.mockResolvedValue({}); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(); // Act @@ -747,7 +766,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocUpdate.mockResolvedValue({}); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); // Act await OfferStatusChangeTrigger.handler(ctx); @@ -776,7 +795,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocUpdate.mockResolvedValue({}); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(); // Act @@ -811,7 +830,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocUpdate.mockResolvedValue({}); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(); // Act @@ -847,7 +866,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocUpdate.mockRejectedValue(new Error('Database error')); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(); // Act & Assert @@ -874,7 +893,7 @@ describe('Offer Hook - OfferStatusChangeTrigger', () => { mockQlDocUpdate.mockResolvedValue({}); mockQlDocGet.mockRejectedValue(new Error('Candidate not found')); - const ctx = createMockContext('afterUpdate', offer, previous); + const ctx = createMockContext('afterUpdate', offer, previous, offer); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(); // Act & Assert diff --git a/packages/hr/package.json b/packages/hr/package.json index 699f2c2fd..cfa092622 100644 --- a/packages/hr/package.json +++ b/packages/hr/package.json @@ -23,11 +23,11 @@ "author": "HotCRM Team", "license": "MIT", "dependencies": { - "@objectstack/spec": "^1.1.0", + "@objectstack/spec": "^2.0.0", "typescript": "^5.3.0" }, "peerDependencies": { - "@objectstack/cli": "^1.1.0" + "@objectstack/cli": "^2.0.0" }, "devDependencies": { "@types/node": "^25.2.0", diff --git a/packages/hr/src/hooks/candidate.hook.ts b/packages/hr/src/hooks/candidate.hook.ts index e219f068e..4901013ec 100644 --- a/packages/hr/src/hooks/candidate.hook.ts +++ b/packages/hr/src/hooks/candidate.hook.ts @@ -13,13 +13,13 @@ const CandidateScoringTrigger: Hook = { name: 'CandidateScoringTrigger', object: 'candidate', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const candidate = ctx.input; + const candidate = (ctx.input.doc as any) || ctx.input; // Check for duplicate candidates (same email) - if (ctx.event === 'beforeInsert' || (ctx.previous && candidate.email !== ctx.previous.email)) { - const duplicates = await ctx.ql.find('candidate', { + if (ctx.event === 'beforeInsert' || (ctx.previous && candidate.email !== (ctx.previous as any).email)) { + const duplicates = await (ctx.ql as any).find('candidate', { filters: [ ['email', '=', candidate.email], ['id', '!=', candidate.id || ''] @@ -122,7 +122,7 @@ function calculateCandidateScore(candidate: any): number { /** * Auto-screen candidate against minimum requirements */ -async function autoScreen(candidate: any, ctx: HookContext): Promise { +async function autoScreen(candidate: any, ctx: any): Promise { // Basic validation if (!candidate.email || !candidate.mobile_phone) { return false; @@ -151,15 +151,15 @@ const CandidateStatusChangeTrigger: Hook = { name: 'CandidateStatusChangeTrigger', object: 'candidate', events: ['afterUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { // Check if status changed - if (!ctx.previous || ctx.previous.status === ctx.input.status) { + if (!ctx.previous || (ctx.previous as any).status === (ctx.result as any).status) { return; } - const candidate = ctx.input; - const oldStatus = ctx.previous.status; + const candidate = ctx.result as any; + const oldStatus = (ctx.previous as any).status; const newStatus = candidate.status; console.log(`๐Ÿ”„ Candidate status changed from "${oldStatus}" to "${newStatus}"`); @@ -192,7 +192,7 @@ const CandidateStatusChangeTrigger: Hook = { /** * Handle transition to Interviewing status */ -async function handleInterviewingStatus(candidate: any, ctx: HookContext): Promise { +async function handleInterviewingStatus(candidate: any, ctx: any): Promise { console.log(`๐Ÿ“… Candidate ${candidate.first_name} ${candidate.last_name} moved to interviewing stage`); // TODO: Schedule first interview @@ -203,7 +203,7 @@ async function handleInterviewingStatus(candidate: any, ctx: HookContext): Promi /** * Handle transition to Hired status */ -async function handleHiredStatus(candidate: any, ctx: HookContext): Promise { +async function handleHiredStatus(candidate: any, ctx: any): Promise { console.log(`๐ŸŽ‰ Candidate ${candidate.first_name} ${candidate.last_name} has been hired`); // TODO: Create offer record if not exists @@ -215,7 +215,7 @@ async function handleHiredStatus(candidate: any, ctx: HookContext): Promise { +async function handleRejectedStatus(candidate: any, ctx: any): Promise { console.log(`โŒ Candidate ${candidate.first_name} ${candidate.last_name} has been rejected`); // TODO: Send rejection email (if configured) @@ -225,7 +225,7 @@ async function handleRejectedStatus(candidate: any, ctx: HookContext): Promise { +async function handleWithdrawnStatus(candidate: any, ctx: any): Promise { console.log(`๐Ÿšช Candidate ${candidate.first_name} ${candidate.last_name} has withdrawn`); // TODO: Log withdrawal reason @@ -239,7 +239,7 @@ async function logStatusChange( candidate: any, oldStatus: string, newStatus: string, - ctx: HookContext + ctx: any ): Promise { try { // Note: Activity object might need to be adapted for HR module diff --git a/packages/hr/src/hooks/employee.hook.ts b/packages/hr/src/hooks/employee.hook.ts index 2512eb086..d0839f261 100644 --- a/packages/hr/src/hooks/employee.hook.ts +++ b/packages/hr/src/hooks/employee.hook.ts @@ -13,9 +13,9 @@ const EmployeeOnboardingTrigger: Hook = { name: 'EmployeeOnboardingTrigger', object: 'employee', events: ['afterInsert'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const employee = ctx.input; + const employee = ctx.result as any; console.log(`๐Ÿ‘‹ Initiating onboarding for employee: ${employee.first_name} ${employee.last_name}`); @@ -40,7 +40,7 @@ const EmployeeOnboardingTrigger: Hook = { /** * Create onboarding record for new employee */ -async function createOnboardingRecord(employee: any, ctx: HookContext): Promise { +async function createOnboardingRecord(employee: any, ctx: any): Promise { try { // Calculate onboarding completion date (typically 30-90 days) const startDate = new Date(employee.hire_date || new Date()); @@ -66,7 +66,7 @@ async function createOnboardingRecord(employee: any, ctx: HookContext): Promise< /** * Notify manager about new team member */ -async function notifyManager(employee: any, ctx: HookContext): Promise { +async function notifyManager(employee: any, ctx: any): Promise { try { if (!employee.manager_id) { console.log('โš ๏ธ No manager assigned, skipping notification'); @@ -91,13 +91,13 @@ async function notifyManager(employee: any, ctx: HookContext): Promise { /** * Create initial goals for probation period */ -async function createProbationGoals(employee: any, ctx: HookContext): Promise { +async function createProbationGoals(employee: any, ctx: any): Promise { try { const hireDate = new Date(employee.hire_date || new Date()); const probationEndDate = new Date(hireDate); probationEndDate.setDate(probationEndDate.getDate() + 90); // 90-day probation - await ctx.ql.doc.create('goal', { + await (ctx.ql as any).doc.create('goal', { employee_id: employee.id, goal_name: `Complete Onboarding - ${employee.full_name}`, description: 'Successfully complete all onboarding tasks and probation requirements', @@ -123,15 +123,15 @@ const EmployeeStatusChangeTrigger: Hook = { name: 'EmployeeStatusChangeTrigger', object: 'employee', events: ['afterUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { // Check if employment status changed - if (!ctx.previous || ctx.previous.employment_status === ctx.input.employment_status) { + if (!ctx.previous || (ctx.previous as any).employment_status === (ctx.result as any).employment_status) { return; } - const employee = ctx.input; - const oldStatus = ctx.previous.employment_status; + const employee = ctx.result as any; + const oldStatus = (ctx.previous as any).employment_status; const newStatus = employee.employment_status; console.log(`๐Ÿ”„ Employee status changed from "${oldStatus}" to "${newStatus}"`); @@ -162,7 +162,7 @@ const EmployeeStatusChangeTrigger: Hook = { /** * Handle employee activation */ -async function handleActivation(employee: any, ctx: HookContext): Promise { +async function handleActivation(employee: any, ctx: any): Promise { console.log(`โœ… Employee ${employee.full_name} activated`); // TODO: Enable system accounts @@ -173,12 +173,12 @@ async function handleActivation(employee: any, ctx: HookContext): Promise /** * Handle employee termination */ -async function handleTermination(employee: any, ctx: HookContext): Promise { +async function handleTermination(employee: any, ctx: any): Promise { console.log(`๐Ÿ‘‹ Processing termination for ${employee.full_name}`); try { // Create offboarding record - await ctx.ql.doc.create('onboarding', { + await (ctx.ql as any).doc.create('onboarding', { employee_id: employee.id, start_date: new Date().toISOString().split('T')[0], status: 'In Progress', @@ -201,7 +201,7 @@ async function handleTermination(employee: any, ctx: HookContext): Promise /** * Handle leave start */ -async function handleLeaveStart(employee: any, ctx: HookContext): Promise { +async function handleLeaveStart(employee: any, ctx: any): Promise { console.log(`๐Ÿ–๏ธ Employee ${employee.full_name} started leave`); // TODO: Update team calendars @@ -216,7 +216,7 @@ async function logEmployeeStatusChange( employee: any, oldStatus: string, newStatus: string, - ctx: HookContext + ctx: any ): Promise { try { console.log(`๐Ÿ“ Logging status change: ${oldStatus} โ†’ ${newStatus} for ${employee.full_name}`); @@ -236,9 +236,9 @@ const EmployeeDataValidationTrigger: Hook = { name: 'EmployeeDataValidationTrigger', object: 'employee', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const employee = ctx.input; + const employee = (ctx.input.doc as any) || ctx.input; // Validate hire date is not in future if (employee.hire_date) { diff --git a/packages/hr/src/hooks/offer.hook.ts b/packages/hr/src/hooks/offer.hook.ts index 6a53413ae..66a629fb4 100644 --- a/packages/hr/src/hooks/offer.hook.ts +++ b/packages/hr/src/hooks/offer.hook.ts @@ -13,9 +13,9 @@ const OfferCreationTrigger: Hook = { name: 'OfferCreationTrigger', object: 'offer', events: ['beforeInsert'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const offer = ctx.input; + const offer = (ctx.input.doc as any) || ctx.input; // Generate offer number if not provided if (!offer.offer_number) { @@ -47,12 +47,12 @@ const OfferCreationTrigger: Hook = { /** * Generate unique offer number */ -async function generateOfferNumber(ctx: HookContext): Promise { +async function generateOfferNumber(ctx: any): Promise { const year = new Date().getFullYear(); const month = String(new Date().getMonth() + 1).padStart(2, '0'); // Find latest offer number for this year/month - const latestOffers = await ctx.ql.find('offer', { + const latestOffers = await (ctx.ql as any).find('offer', { filters: [ ['offer_number', 'like', `OFF-${year}${month}%`] ], @@ -79,15 +79,15 @@ const OfferStatusChangeTrigger: Hook = { name: 'OfferStatusChangeTrigger', object: 'offer', events: ['afterUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { // Check if status changed - if (!ctx.previous || ctx.previous.status === ctx.input.status) { + if (!ctx.previous || (ctx.previous as any).status === (ctx.result as any).status) { return; } - const offer = ctx.input; - const oldStatus = ctx.previous.status; + const offer = ctx.result as any; + const oldStatus = (ctx.previous as any).status; const newStatus = offer.status; console.log(`๐Ÿ”„ Offer ${offer.offer_number} status changed from "${oldStatus}" to "${newStatus}"`); @@ -123,23 +123,23 @@ const OfferStatusChangeTrigger: Hook = { /** * Handle offer sent */ -async function handleOfferSent(offer: any, ctx: HookContext): Promise { +async function handleOfferSent(offer: any, ctx: any): Promise { console.log(`๐Ÿ“ง Offer ${offer.offer_number} sent to candidate`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'Offer Sent' }); // Update application status - await ctx.ql.doc.update('application', offer.application_id, { + await (ctx.ql as any).doc.update('application', offer.application_id, { status: 'Offer Sent' }); // Set sent date if not already set if (!offer.sent_date) { - await ctx.ql.doc.update('offer', offer.id, { + await (ctx.ql as any).doc.update('offer', offer.id, { sent_date: new Date().toISOString().split('T')[0] }); } @@ -158,23 +158,23 @@ async function handleOfferSent(offer: any, ctx: HookContext): Promise { /** * Handle offer accepted */ -async function handleOfferAccepted(offer: any, ctx: HookContext): Promise { +async function handleOfferAccepted(offer: any, ctx: any): Promise { console.log(`๐ŸŽ‰ Offer ${offer.offer_number} accepted by candidate`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'Hired' }); // Update application status - await ctx.ql.doc.update('application', offer.application_id, { + await (ctx.ql as any).doc.update('application', offer.application_id, { status: 'Hired' }); // Set acceptance date if not already set if (!offer.acceptance_date) { - await ctx.ql.doc.update('offer', offer.id, { + await (ctx.ql as any).doc.update('offer', offer.id, { acceptance_date: new Date().toISOString().split('T')[0] }); } @@ -196,10 +196,10 @@ async function handleOfferAccepted(offer: any, ctx: HookContext): Promise /** * Create employee record from accepted offer */ -async function createEmployeeFromOffer(offer: any, ctx: HookContext): Promise { +async function createEmployeeFromOffer(offer: any, ctx: any): Promise { try { // Fetch candidate details - const candidate = await ctx.ql.doc.get('candidate', offer.candidate_id, { + const candidate = await (ctx.ql as any).doc.get('candidate', offer.candidate_id, { fields: ['first_name', 'last_name', 'email', 'mobile_phone', 'date_of_birth', 'gender'] }); @@ -211,7 +211,7 @@ async function createEmployeeFromOffer(offer: any, ctx: HookContext): Promise { +async function generateEmployeeNumber(ctx: any): Promise { const year = new Date().getFullYear(); // Find latest employee number for this year - const latestEmployees = await ctx.ql.find('employee', { + const latestEmployees = await (ctx.ql as any).find('employee', { filters: [ ['employee_number', 'like', `EMP${year}%`] ], @@ -270,23 +270,23 @@ async function generateEmployeeNumber(ctx: HookContext): Promise { /** * Handle offer rejected */ -async function handleOfferRejected(offer: any, ctx: HookContext): Promise { +async function handleOfferRejected(offer: any, ctx: any): Promise { console.log(`โŒ Offer ${offer.offer_number} rejected by candidate`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'Offer Rejected' }); // Update application status - await ctx.ql.doc.update('application', offer.application_id, { + await (ctx.ql as any).doc.update('application', offer.application_id, { status: 'Rejected' }); // Set rejection date if not already set if (!offer.rejection_date) { - await ctx.ql.doc.update('offer', offer.id, { + await (ctx.ql as any).doc.update('offer', offer.id, { rejection_date: new Date().toISOString().split('T')[0] }); } @@ -303,7 +303,7 @@ async function handleOfferRejected(offer: any, ctx: HookContext): Promise /** * Handle offer expired */ -async function handleOfferExpired(offer: any, ctx: HookContext): Promise { +async function handleOfferExpired(offer: any, ctx: any): Promise { console.log(`โฐ Offer ${offer.offer_number} has expired`); try { @@ -323,12 +323,12 @@ async function handleOfferExpired(offer: any, ctx: HookContext): Promise { /** * Handle offer withdrawn */ -async function handleOfferWithdrawn(offer: any, ctx: HookContext): Promise { +async function handleOfferWithdrawn(offer: any, ctx: any): Promise { console.log(`๐Ÿšซ Offer ${offer.offer_number} has been withdrawn`); try { // Update candidate status - await ctx.ql.doc.update('candidate', offer.candidate_id, { + await (ctx.ql as any).doc.update('candidate', offer.candidate_id, { status: 'Offer Withdrawn' }); @@ -347,7 +347,7 @@ async function logOfferStatusChange( offer: any, oldStatus: string, newStatus: string, - ctx: HookContext + ctx: any ): Promise { try { console.log(`๐Ÿ“ Logging offer status change: ${oldStatus} โ†’ ${newStatus} for offer ${offer.offer_number}`); @@ -367,10 +367,10 @@ const OfferApprovalTrigger: Hook = { name: 'OfferApprovalTrigger', object: 'offer', events: ['beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const offer = ctx.input; - const previousOffer = ctx.previous; + const offer = (ctx.input.doc as any) || ctx.input; + const previousOffer = ctx.previous as any; // Check if approval status changed if (!previousOffer || previousOffer.approval_status === offer.approval_status) { diff --git a/packages/hr/src/hooks/performance_review.hook.ts b/packages/hr/src/hooks/performance_review.hook.ts index 32263823f..ce40d36b6 100644 --- a/packages/hr/src/hooks/performance_review.hook.ts +++ b/packages/hr/src/hooks/performance_review.hook.ts @@ -22,9 +22,9 @@ const PerformanceReviewRatingTrigger: Hook = { name: 'PerformanceReviewRatingTrigger', object: 'performance_review', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const review = ctx.input; + const review = (ctx.input.doc as any) || ctx.input; // Calculate overall rating from component scores if (hasAllComponentScores(review)) { @@ -132,15 +132,15 @@ const PerformanceReviewWorkflowTrigger: Hook = { name: 'PerformanceReviewWorkflowTrigger', object: 'performance_review', events: ['afterUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { // Check if status changed - if (!ctx.previous || ctx.previous.status === ctx.input.status) { + if (!ctx.previous || (ctx.previous as any).status === (ctx.result as any).status) { return; } - const review = ctx.input; - const oldStatus = ctx.previous.status; + const review = ctx.result as any; + const oldStatus = (ctx.previous as any).status; const newStatus = review.status; console.log(`๐Ÿ”„ Performance review ${review.review_name} status changed from "${oldStatus}" to "${newStatus}"`); @@ -176,7 +176,7 @@ const PerformanceReviewWorkflowTrigger: Hook = { /** * Handle review started */ -async function handleReviewStarted(review: any, ctx: HookContext): Promise { +async function handleReviewStarted(review: any, ctx: any): Promise { console.log(`๐Ÿ“ Performance review started: ${review.review_name}`); try { @@ -196,12 +196,12 @@ async function handleReviewStarted(review: any, ctx: HookContext): Promise /** * Handle review submitted for approval */ -async function handleReviewSubmitted(review: any, ctx: HookContext): Promise { +async function handleReviewSubmitted(review: any, ctx: any): Promise { console.log(`๐Ÿ“ค Performance review submitted for approval: ${review.review_name}`); try { // Set submission date - await ctx.ql.doc.update('performance_review', review.id, { + await (ctx.ql as any).doc.update('performance_review', review.id, { submitted_date: new Date().toISOString().split('T')[0], submitted_by: ctx.session?.userId }); @@ -219,12 +219,12 @@ async function handleReviewSubmitted(review: any, ctx: HookContext): Promise { +async function handleReviewApproved(review: any, ctx: any): Promise { console.log(`โœ… Performance review approved: ${review.review_name}`); try { // Set approval date - await ctx.ql.doc.update('performance_review', review.id, { + await (ctx.ql as any).doc.update('performance_review', review.id, { approved_date: new Date().toISOString().split('T')[0], approved_by: ctx.session?.userId }); @@ -247,7 +247,7 @@ async function handleReviewApproved(review: any, ctx: HookContext): Promise { +async function triggerCompensationReview(review: any, ctx: any): Promise { try { console.log(`๐Ÿ’ฐ Triggering compensation review for high performer: ${review.employee_id}`); @@ -263,7 +263,7 @@ async function triggerCompensationReview(review: any, ctx: HookContext): Promise /** * Create development goals from review feedback */ -async function createDevelopmentGoals(review: any, ctx: HookContext): Promise { +async function createDevelopmentGoals(review: any, ctx: any): Promise { try { if (!review.development_plan || review.development_plan.trim() === '') { console.log('โ„น๏ธ No development plan specified, skipping goal creation'); @@ -290,7 +290,7 @@ async function createDevelopmentGoals(review: any, ctx: HookContext): Promise { +async function handleReviewCompleted(review: any, ctx: any): Promise { console.log(`๐ŸŽ‰ Performance review completed: ${review.review_name}`); try { // Set completion date - await ctx.ql.doc.update('performance_review', review.id, { + await (ctx.ql as any).doc.update('performance_review', review.id, { completion_date: new Date().toISOString().split('T')[0] }); @@ -337,9 +337,9 @@ async function handleReviewCompleted(review: any, ctx: HookContext): Promise { +async function updateEmployeeReviewStats(review: any, ctx: any): Promise { try { - await ctx.ql.doc.update('employee', review.employee_id, { + await (ctx.ql as any).doc.update('employee', review.employee_id, { last_review_date: review.end_date, last_review_rating: review.overall_rating, last_review_level: review.performance_level @@ -355,12 +355,12 @@ async function updateEmployeeReviewStats(review: any, ctx: HookContext): Promise /** * Handle review rejected */ -async function handleReviewRejected(review: any, ctx: HookContext): Promise { +async function handleReviewRejected(review: any, ctx: any): Promise { console.log(`โŒ Performance review rejected: ${review.review_name}`); try { // Send back to reviewer for revision - await ctx.ql.doc.update('performance_review', review.id, { + await (ctx.ql as any).doc.update('performance_review', review.id, { status: 'In Progress' }); @@ -378,7 +378,7 @@ async function logReviewStatusChange( review: any, oldStatus: string, newStatus: string, - ctx: HookContext + ctx: any ): Promise { try { console.log(`๐Ÿ“ Logging review status change: ${oldStatus} โ†’ ${newStatus} for ${review.review_name}`); @@ -395,14 +395,14 @@ async function logReviewStatusChange( * Checks for upcoming due dates and sends reminders * (This would typically be called by a scheduled job, not a trigger) */ -export async function checkPerformanceReviewDueDates(ctx: HookContext): Promise { +export async function checkPerformanceReviewDueDates(ctx: any): Promise { try { const today = new Date(); const reminderDate = new Date(today); reminderDate.setDate(reminderDate.getDate() + 7); // 7 days before due // Find reviews due soon - const upcomingReviews = await ctx.ql.find('performance_review', { + const upcomingReviews = await (ctx.ql as any).find('performance_review', { filters: [ ['status', 'in', ['Not Started', 'In Progress']], ['due_date', '<=', reminderDate.toISOString().split('T')[0]], diff --git a/packages/marketing/package.json b/packages/marketing/package.json index 8a553922c..ccf5473b2 100644 --- a/packages/marketing/package.json +++ b/packages/marketing/package.json @@ -25,10 +25,10 @@ "author": "HotCRM Team", "license": "MIT", "dependencies": { - "@objectstack/spec": "^1.1.0" + "@objectstack/spec": "^2.0.0" }, "peerDependencies": { - "@objectstack/cli": "^1.1.0" + "@objectstack/cli": "^2.0.0" }, "devDependencies": { "typescript": "^5.0.0" diff --git a/packages/marketing/src/hooks/campaign.hook.ts b/packages/marketing/src/hooks/campaign.hook.ts index d5d5f71ff..a85e31968 100644 --- a/packages/marketing/src/hooks/campaign.hook.ts +++ b/packages/marketing/src/hooks/campaign.hook.ts @@ -1,4 +1,4 @@ -import type { Hook, HookContext } from '@objectstack/spec/data'; +import type { Hook } from '@objectstack/spec/data'; /** * Campaign Lifecycle and ROI Management Trigger @@ -13,9 +13,9 @@ const CampaignROICalculationTrigger: Hook = { name: 'CampaignROICalculationTrigger', object: 'campaign', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const campaign = ctx.input; + const campaign = ctx.input?.doc || ctx.input; // Calculate ROI: (ActualRevenue - ActualCost) / ActualCost * 100 const actualRevenue = campaign.actual_revenue || 0; @@ -57,9 +57,9 @@ const CampaignBudgetTrackingTrigger: Hook = { name: 'CampaignBudgetTrackingTrigger', object: 'campaign', events: ['beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const campaign = ctx.input; + const campaign = ctx.input?.doc || ctx.input; const oldCampaign = ctx.previous; // Only process if actual cost changed @@ -106,9 +106,9 @@ const CampaignStatusChangeTrigger: Hook = { name: 'CampaignStatusChangeTrigger', object: 'campaign', events: ['afterUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const campaign = ctx.input; + const campaign = ctx.result || ctx.input?.doc || ctx.input; const oldCampaign = ctx.previous; // Check if status changed @@ -139,7 +139,7 @@ const CampaignStatusChangeTrigger: Hook = { /** * Handle campaign transition to In Progress */ -async function handleInProgressStatus(campaign: any, ctx: HookContext): Promise { +async function handleInProgressStatus(campaign: any, ctx: any): Promise { console.log(`๐Ÿš€ Campaign "${campaign.name}" started`); // TODO: Send notifications to campaign members @@ -150,7 +150,7 @@ async function handleInProgressStatus(campaign: any, ctx: HookContext): Promise< /** * Handle campaign completion */ -async function handleCompletedStatus(campaign: any, ctx: HookContext): Promise { +async function handleCompletedStatus(campaign: any, ctx: any): Promise { console.log(`โœ… Campaign "${campaign.name}" completed`); // TODO: Calculate final performance metrics @@ -162,7 +162,7 @@ async function handleCompletedStatus(campaign: any, ctx: HookContext): Promise { +async function handleAbortedStatus(campaign: any, ctx: any): Promise { console.log(`โŒ Campaign "${campaign.name}" was aborted`); // TODO: Log abort reason @@ -179,9 +179,9 @@ const CampaignDateValidationTrigger: Hook = { name: 'CampaignDateValidationTrigger', object: 'campaign', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const campaign = ctx.input; + const campaign = ctx.input?.doc || ctx.input; if (campaign.start_date && campaign.end_date) { const startDate = new Date(campaign.start_date); @@ -210,12 +210,12 @@ const CampaignDateValidationTrigger: Hook = { * Update campaign metrics from member engagement * Called by campaign_member hooks */ -export async function updateCampaignMetrics(campaignId: string, ctx: HookContext): Promise { +export async function updateCampaignMetrics(campaignId: string, ctx: any): Promise { try { console.log(`๐Ÿ”„ Updating campaign metrics for: ${campaignId}`); // Aggregate campaign member statistics - const members = await ctx.ql.find('campaign_member', { + const members = await (ctx.ql as any).find('campaign_member', { filters: [['campaign', '=', campaignId]] }); @@ -241,7 +241,7 @@ export async function updateCampaignMetrics(campaignId: string, ctx: HookContext const unsubscribeRate = totalMembers > 0 ? (unsubscribed / totalMembers) * 100 : 0; // Update campaign with aggregated metrics - await ctx.ql.update('campaign', campaignId, { + await (ctx.ql as any).update('campaign', campaignId, { total_members: totalMembers, total_opened: opened, total_clicked: clicked, @@ -263,9 +263,9 @@ export async function updateCampaignMetrics(campaignId: string, ctx: HookContext /** * Calculate cost per lead/opportunity/customer */ -export async function calculateCostMetrics(campaignId: string, ctx: HookContext): Promise { +export async function calculateCostMetrics(campaignId: string, ctx: any): Promise { try { - const campaign = await ctx.ql.findOne('campaign', { + const campaign = await (ctx.ql as any).findOne('campaign', { filters: [['id', '=', campaignId]] }); diff --git a/packages/marketing/src/hooks/campaign_member.hook.ts b/packages/marketing/src/hooks/campaign_member.hook.ts index 99f2a1bc6..22df613a6 100644 --- a/packages/marketing/src/hooks/campaign_member.hook.ts +++ b/packages/marketing/src/hooks/campaign_member.hook.ts @@ -1,4 +1,4 @@ -import type { Hook, HookContext } from '@objectstack/spec/data'; +import type { Hook } from '@objectstack/spec/data'; import { updateCampaignMetrics } from './campaign.hook'; /** @@ -14,10 +14,10 @@ const CampaignMemberEngagementTrigger: Hook = { name: 'CampaignMemberEngagementTrigger', object: 'campaign_member', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const member = ctx.input; - const oldMember = ctx.previous; + const member = ctx.input.doc as Record; + const oldMember = ctx.previous as Record | undefined; const now = new Date().toISOString(); @@ -84,10 +84,10 @@ const CampaignMemberLeadScoringTrigger: Hook = { name: 'CampaignMemberLeadScoringTrigger', object: 'campaign_member', events: ['afterUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const member = ctx.input; - const oldMember = ctx.previous; + const member = ctx.result as Record; + const oldMember = ctx.previous as Record | undefined; // Only process if status changed if (!oldMember || oldMember.status === member.status) { @@ -152,9 +152,9 @@ const CampaignMemberStatsTrigger: Hook = { name: 'CampaignMemberStatsTrigger', object: 'campaign_member', events: ['afterInsert', 'afterUpdate', 'afterDelete'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const member = ctx.input; + const member = ctx.result as Record; const campaignId = member?.campaign || ctx.previous?.campaign; if (!campaignId) { @@ -182,10 +182,10 @@ const CampaignMemberBounceHandlerTrigger: Hook = { name: 'CampaignMemberBounceHandlerTrigger', object: 'campaign_member', events: ['beforeUpdate'], - handler: async (ctx: HookContext) => { + handler: async (ctx: any) => { try { - const member = ctx.input; - const oldMember = ctx.previous; + const member = ctx.input.doc as Record; + const oldMember = ctx.previous as Record | undefined; // Check if bounce information was added if (!oldMember?.email_bounced_date && member.email_bounced_date) { @@ -287,7 +287,7 @@ export function calculateEngagementScore(member: any): number { export async function trackMemberEngagementTimeline( memberId: string, engagementType: 'opened' | 'clicked' | 'responded' | 'unsubscribed', - ctx: HookContext + ctx: any ): Promise { try { console.log(`๐Ÿ”„ Tracking engagement timeline for member: ${memberId}`); diff --git a/packages/marketing/src/hooks/roi.hook.ts b/packages/marketing/src/hooks/roi.hook.ts index 6742b5e86..64ad1f640 100644 --- a/packages/marketing/src/hooks/roi.hook.ts +++ b/packages/marketing/src/hooks/roi.hook.ts @@ -11,14 +11,14 @@ const CampaignROIHook: Hook = { object: 'opportunity', // Listening to Opportunity events from Marketing package events: ['afterUpdate'], handler: async (ctx: any) => { - const newOpp = ctx.new; - const oldOpp = ctx.old; + const newOpp = ctx.result as Record; + const oldOpp = ctx.previous as Record | undefined; // Check if Campaign is linked if (!newOpp.campaign_id) return; const isWon = newOpp.stage_name === 'closed_won'; - const wasWon = oldOpp.stage_name === 'closed_won'; + const wasWon = oldOpp?.stage_name === 'closed_won'; let delta = 0; @@ -28,11 +28,11 @@ const CampaignROIHook: Hook = { } // Case 2: Just Lost (or moved out of Won) else if (!isWon && wasWon) { - delta = -(oldOpp.amount || 0); + delta = -(oldOpp?.amount || 0); } // Case 3: Amount changed while Won - else if (isWon && wasWon && newOpp.amount !== oldOpp.amount) { - delta = (newOpp.amount || 0) - (oldOpp.amount || 0); + else if (isWon && wasWon && newOpp.amount !== oldOpp?.amount) { + delta = (newOpp.amount || 0) - (oldOpp?.amount || 0); } if (delta !== 0) { diff --git a/packages/products/package.json b/packages/products/package.json index 2f62559fc..566a0c58b 100644 --- a/packages/products/package.json +++ b/packages/products/package.json @@ -21,11 +21,11 @@ "author": "HotCRM Team", "license": "MIT", "dependencies": { - "@objectstack/spec": "^1.1.0", + "@objectstack/spec": "^2.0.0", "typescript": "^5.3.0" }, "peerDependencies": { - "@objectstack/cli": "^1.1.0" + "@objectstack/cli": "^2.0.0" }, "devDependencies": { "@types/node": "^25.2.0", diff --git a/packages/products/src/hooks/pricebook.hook.ts b/packages/products/src/hooks/pricebook.hook.ts index 049edc9d7..872512b04 100644 --- a/packages/products/src/hooks/pricebook.hook.ts +++ b/packages/products/src/hooks/pricebook.hook.ts @@ -1,14 +1,7 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; -// Types for Context -export interface TriggerContext { - old?: Record; - new: Record; - db: typeof db; - user: { id: string; name: string; email: string; }; - event?: string; // Event type: beforeInsert, beforeUpdate, afterUpdate -} + /** * Pricebook Hook @@ -23,16 +16,20 @@ const PricebookHook: Hook = { name: 'PricebookHook', object: 'Pricebook', events: ['beforeInsert', 'beforeUpdate', 'afterUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { + // Determine if this is a before or after hook based on available properties + const isBeforeHook = !!ctx.input; + const isAfterHook = !!ctx.result; + // Before Insert/Update: Validate pricebook configuration - if (ctx.event === 'beforeInsert' || ctx.event === 'beforeUpdate') { + if (isBeforeHook) { await validatePricebookDates(ctx); await validateCurrencyConfiguration(ctx); } // After Update: Handle effective date and status changes - if (ctx.event === 'afterUpdate') { + if (isAfterHook) { await handleEffectiveDateChange(ctx); await handleStatusChange(ctx); await handleCurrencyChange(ctx); @@ -48,8 +45,8 @@ const PricebookHook: Hook = { /** * Validate pricebook date ranges */ -async function validatePricebookDates(ctx: TriggerContext): Promise { - const pricebook = ctx.new; +async function validatePricebookDates(ctx: any): Promise { + const pricebook = ctx.input?.doc || ctx.result; try { // Validate effective dates @@ -88,8 +85,8 @@ async function validatePricebookDates(ctx: TriggerContext): Promise { /** * Validate currency configuration */ -async function validateCurrencyConfiguration(ctx: TriggerContext): Promise { - const pricebook = ctx.new; +async function validateCurrencyConfiguration(ctx: any): Promise { + const pricebook = ctx.input?.doc || ctx.result; try { // Validate currency code @@ -120,15 +117,15 @@ async function validateCurrencyConfiguration(ctx: TriggerContext): Promise /** * Handle effective date changes */ -async function handleEffectiveDateChange(ctx: TriggerContext): Promise { - if (!ctx.old || !ctx.new) return; +async function handleEffectiveDateChange(ctx: any): Promise { + if (!ctx.previous || !ctx.result) return; - const effectiveDateChanged = ctx.old.EffectiveDate !== ctx.new.EffectiveDate; - const expirationDateChanged = ctx.old.ExpirationDate !== ctx.new.ExpirationDate; + const effectiveDateChanged = ctx.previous.EffectiveDate !== ctx.result.EffectiveDate; + const expirationDateChanged = ctx.previous.ExpirationDate !== ctx.result.ExpirationDate; if (!effectiveDateChanged && !expirationDateChanged) return; - const pricebook = ctx.new; + const pricebook = ctx.result; const today = new Date(); today.setHours(0, 0, 0, 0); @@ -190,16 +187,16 @@ async function handleEffectiveDateChange(ctx: TriggerContext): Promise { /** * Handle pricebook status changes */ -async function handleStatusChange(ctx: TriggerContext): Promise { - if (!ctx.old || !ctx.new) return; +async function handleStatusChange(ctx: any): Promise { + if (!ctx.previous || !ctx.result) return; - const statusChanged = ctx.old.Status !== ctx.new.Status; + const statusChanged = ctx.previous.Status !== ctx.result.Status; if (!statusChanged) return; - const pricebook = ctx.new; + const pricebook = ctx.result; try { - console.log(`๐Ÿ”„ Pricebook status changed from "${ctx.old.Status}" to "${pricebook.Status}"`); + console.log(`๐Ÿ”„ Pricebook status changed from "${ctx.previous.Status}" to "${pricebook.Status}"`); // Handle activation if (pricebook.Status === 'Active') { @@ -260,14 +257,14 @@ async function handleStatusChange(ctx: TriggerContext): Promise { // Log activity for status change await ctx.db.doc.create('Activity', { - Subject: `Pricebook Status Changed: ${ctx.old.Status} โ†’ ${pricebook.Status}`, + Subject: `Pricebook Status Changed: ${ctx.previous.Status} โ†’ ${pricebook.Status}`, Type: 'Status Change', Status: 'Completed', Priority: 'Normal', WhatId: pricebook.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `Pricebook "${pricebook.Name}" status changed from "${ctx.old.Status}" to "${pricebook.Status}"` + Description: `Pricebook "${pricebook.Name}" status changed from "${ctx.previous.Status}" to "${pricebook.Status}"` }); } catch (error) { console.error('โŒ Error handling status change:', error); @@ -277,19 +274,19 @@ async function handleStatusChange(ctx: TriggerContext): Promise { /** * Handle currency or exchange rate changes */ -async function handleCurrencyChange(ctx: TriggerContext): Promise { - if (!ctx.old || !ctx.new) return; +async function handleCurrencyChange(ctx: any): Promise { + if (!ctx.previous || !ctx.result) return; - const currencyChanged = ctx.old.CurrencyCode !== ctx.new.CurrencyCode; - const rateChanged = ctx.old.ExchangeRate !== ctx.new.ExchangeRate; + const currencyChanged = ctx.previous.CurrencyCode !== ctx.result.CurrencyCode; + const rateChanged = ctx.previous.ExchangeRate !== ctx.result.ExchangeRate; if (!currencyChanged && !rateChanged) return; - const pricebook = ctx.new; + const pricebook = ctx.result; try { if (currencyChanged) { - console.log(`๐Ÿ’ฑ Currency changed from ${ctx.old.CurrencyCode} to ${pricebook.CurrencyCode}`); + console.log(`๐Ÿ’ฑ Currency changed from ${ctx.previous.CurrencyCode} to ${pricebook.CurrencyCode}`); // TODO: Update all pricebook entries to reflect new currency console.log('๐Ÿ’ฑ Pricebook entries would be updated for new currency'); @@ -299,7 +296,7 @@ async function handleCurrencyChange(ctx: TriggerContext): Promise { } if (rateChanged) { - console.log(`๐Ÿ’ฑ Exchange rate changed from ${ctx.old.ExchangeRate} to ${pricebook.ExchangeRate}`); + console.log(`๐Ÿ’ฑ Exchange rate changed from ${ctx.previous.ExchangeRate} to ${pricebook.ExchangeRate}`); // Recalculate all prices based on new exchange rate // This would update all PricebookEntry records @@ -310,10 +307,10 @@ async function handleCurrencyChange(ctx: TriggerContext): Promise { if (currencyChanged || rateChanged) { const changes = []; if (currencyChanged) { - changes.push(`Currency: ${ctx.old.CurrencyCode} โ†’ ${pricebook.CurrencyCode}`); + changes.push(`Currency: ${ctx.previous.CurrencyCode} โ†’ ${pricebook.CurrencyCode}`); } if (rateChanged) { - changes.push(`Exchange Rate: ${ctx.old.ExchangeRate} โ†’ ${pricebook.ExchangeRate}`); + changes.push(`Exchange Rate: ${ctx.previous.ExchangeRate} โ†’ ${pricebook.ExchangeRate}`); } await ctx.db.doc.create('Activity', { diff --git a/packages/products/src/hooks/product.hook.ts b/packages/products/src/hooks/product.hook.ts index 79a475d19..17ca82b54 100644 --- a/packages/products/src/hooks/product.hook.ts +++ b/packages/products/src/hooks/product.hook.ts @@ -1,14 +1,7 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; -// Types for Context -export interface TriggerContext { - old?: Record; - new: Record; - db: typeof db; - user: { id: string; name: string; email: string; }; - event?: string; // Event type: beforeInsert, beforeUpdate, afterUpdate -} + /** * Product Hook @@ -23,16 +16,20 @@ const ProductHook: Hook = { name: 'ProductHook', object: 'Product', events: ['beforeInsert', 'beforeUpdate', 'afterUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { + // Determine if this is a before or after hook based on available properties + const isBeforeHook = !!ctx.input; + const isAfterHook = !!ctx.result; + // Before Insert/Update: Validate product configuration - if (ctx.event === 'beforeInsert' || ctx.event === 'beforeUpdate') { + if (isBeforeHook) { await validateProductConfiguration(ctx); await validateBundleConfiguration(ctx); } // After Update: Handle stock and price changes - if (ctx.event === 'afterUpdate') { + if (isAfterHook) { await handleStockLevelChange(ctx); await handlePriceChange(ctx); await handleStatusChange(ctx); @@ -48,8 +45,8 @@ const ProductHook: Hook = { /** * Validate product configuration */ -async function validateProductConfiguration(ctx: TriggerContext): Promise { - const product = ctx.new; +async function validateProductConfiguration(ctx: any): Promise { + const product = ctx.input?.doc || ctx.result; try { // Ensure product code is unique @@ -83,8 +80,8 @@ async function validateProductConfiguration(ctx: TriggerContext): Promise /** * Validate bundle configuration and dependencies */ -async function validateBundleConfiguration(ctx: TriggerContext): Promise { - const product = ctx.new; +async function validateBundleConfiguration(ctx: any): Promise { + const product = ctx.input?.doc || ctx.result; try { // If this is a bundle product, validate bundle items @@ -112,16 +109,16 @@ async function validateBundleConfiguration(ctx: TriggerContext): Promise { /** * Handle stock level changes */ -async function handleStockLevelChange(ctx: TriggerContext): Promise { - if (!ctx.old || !ctx.new) return; +async function handleStockLevelChange(ctx: any): Promise { + if (!ctx.previous || !ctx.result) return; - const stockChanged = ctx.old.StockLevel !== ctx.new.StockLevel; + const stockChanged = ctx.previous.StockLevel !== ctx.result.StockLevel; if (!stockChanged) return; - const product = ctx.new; + const product = ctx.result; try { - console.log(`๐Ÿ“Š Stock level changed from ${ctx.old.StockLevel} to ${product.StockLevel}`); + console.log(`๐Ÿ“Š Stock level changed from ${ctx.previous.StockLevel} to ${product.StockLevel}`); // Check if stock is low if (product.LowStockThreshold && product.StockLevel <= product.LowStockThreshold) { @@ -146,7 +143,7 @@ async function handleStockLevelChange(ctx: TriggerContext): Promise { } // If stock was replenished, reactivate product - if (ctx.old.StockLevel === 0 && product.StockLevel > 0 && product.Status === 'Out of Stock') { + if (ctx.previous.StockLevel === 0 && product.StockLevel > 0 && product.Status === 'Out of Stock') { await ctx.db.doc.update('Product', product.Id, { Status: 'Active' }); @@ -160,19 +157,19 @@ async function handleStockLevelChange(ctx: TriggerContext): Promise { /** * Handle price changes */ -async function handlePriceChange(ctx: TriggerContext): Promise { - if (!ctx.old || !ctx.new) return; +async function handlePriceChange(ctx: any): Promise { + if (!ctx.previous || !ctx.result) return; - const listPriceChanged = ctx.old.ListPrice !== ctx.new.ListPrice; - const costPriceChanged = ctx.old.CostPrice !== ctx.new.CostPrice; + const listPriceChanged = ctx.previous.ListPrice !== ctx.result.ListPrice; + const costPriceChanged = ctx.previous.CostPrice !== ctx.result.CostPrice; if (!listPriceChanged && !costPriceChanged) return; - const product = ctx.new; + const product = ctx.result; try { if (listPriceChanged) { - console.log(`๐Ÿ’ฐ List price changed from ${ctx.old.ListPrice} to ${product.ListPrice}`); + console.log(`๐Ÿ’ฐ List price changed from ${ctx.previous.ListPrice} to ${product.ListPrice}`); // Log price change activity await ctx.db.doc.create('Activity', { @@ -183,7 +180,7 @@ async function handlePriceChange(ctx: TriggerContext): Promise { WhatId: product.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `List price changed from ${ctx.old.ListPrice} to ${product.ListPrice}` + Description: `List price changed from ${ctx.previous.ListPrice} to ${product.ListPrice}` }); console.log('โœ… Price change activity logged'); @@ -195,7 +192,7 @@ async function handlePriceChange(ctx: TriggerContext): Promise { } if (costPriceChanged) { - console.log(`๐Ÿ’ต Cost price changed from ${ctx.old.CostPrice} to ${product.CostPrice}`); + console.log(`๐Ÿ’ต Cost price changed from ${ctx.previous.CostPrice} to ${product.CostPrice}`); // Recalculate margin if (product.ListPrice && product.CostPrice) { @@ -215,16 +212,16 @@ async function handlePriceChange(ctx: TriggerContext): Promise { /** * Handle product status changes */ -async function handleStatusChange(ctx: TriggerContext): Promise { - if (!ctx.old || !ctx.new) return; +async function handleStatusChange(ctx: any): Promise { + if (!ctx.previous || !ctx.result) return; - const statusChanged = ctx.old.Status !== ctx.new.Status; + const statusChanged = ctx.previous.Status !== ctx.result.Status; if (!statusChanged) return; - const product = ctx.new; + const product = ctx.result; try { - console.log(`๐Ÿ”„ Product status changed from "${ctx.old.Status}" to "${product.Status}"`); + console.log(`๐Ÿ”„ Product status changed from "${ctx.previous.Status}" to "${product.Status}"`); // Handle deactivation if (product.Status === 'Inactive' || product.Status === 'Discontinued') { @@ -238,7 +235,7 @@ async function handleStatusChange(ctx: TriggerContext): Promise { } // Handle reactivation - if ((ctx.old.Status === 'Inactive' || ctx.old.Status === 'Discontinued') && + if ((ctx.previous.Status === 'Inactive' || ctx.previous.Status === 'Discontinued') && product.Status === 'Active') { console.log(`โœ… Product reactivated: ${product.Name}`); @@ -248,14 +245,14 @@ async function handleStatusChange(ctx: TriggerContext): Promise { // Log activity for status change await ctx.db.doc.create('Activity', { - Subject: `Product Status Changed: ${ctx.old.Status} โ†’ ${product.Status}`, + Subject: `Product Status Changed: ${ctx.previous.Status} โ†’ ${product.Status}`, Type: 'Status Change', Status: 'Completed', Priority: 'Normal', WhatId: product.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `Product "${product.Name}" status changed from "${ctx.old.Status}" to "${product.Status}"` + Description: `Product "${product.Name}" status changed from "${ctx.previous.Status}" to "${product.Status}"` }); } catch (error) { console.error('โŒ Error handling status change:', error); diff --git a/packages/products/src/hooks/quote.hook.ts b/packages/products/src/hooks/quote.hook.ts index c6198d071..5f6b44f62 100644 --- a/packages/products/src/hooks/quote.hook.ts +++ b/packages/products/src/hooks/quote.hook.ts @@ -1,14 +1,7 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; -// Types for Context -export interface TriggerContext { - old?: Record; - new: Record; - db: typeof db; - user: { id: string; name: string; email: string; }; - event?: string; // Event type: beforeInsert, beforeUpdate, afterInsert, afterUpdate -} + /** * Quote Pricing Calculation and Approval Routing Hook @@ -24,26 +17,29 @@ const QuotePricingHook: Hook = { name: 'QuotePricingHook', object: 'quote', events: ['beforeInsert', 'beforeUpdate', 'afterInsert', 'afterUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const isInsert = !ctx.old; - const isUpdate = !!ctx.old; - const quote = ctx.new; + // Determine if this is a before or after hook based on available properties + const isBeforeHook = !!ctx.input; + const isAfterHook = !!ctx.result; + const isInsert = !ctx.previous; + const isUpdate = !!ctx.previous; + const quote = (isBeforeHook ? ctx.input?.doc : ctx.result) as Record; // Before Insert/Update: Calculate pricing - if (ctx.event === 'beforeInsert' || ctx.event === 'beforeUpdate') { + if (isBeforeHook) { await calculateQuotePricing(ctx); await determineApprovalRequirements(ctx); await validateMarginProtection(ctx); } // After Insert: Initialize quote - if (ctx.event === 'afterInsert') { + if (isAfterHook && isInsert) { await initializeQuote(ctx); } // After Update: Handle status changes - if (ctx.event === 'afterUpdate') { + if (isAfterHook && isUpdate) { await handleQuoteStatusChange(ctx); await handleApprovalStatusChange(ctx); } @@ -58,8 +54,8 @@ const QuotePricingHook: Hook = { /** * Calculate quote pricing based on line items and discounts */ -async function calculateQuotePricing(ctx: TriggerContext): Promise { - const quote = ctx.new; +async function calculateQuotePricing(ctx: any): Promise { + const quote = ctx.input?.doc || ctx.result; try { // Calculate Discount Amount from Percent or vice versa @@ -93,8 +89,8 @@ async function calculateQuotePricing(ctx: TriggerContext): Promise { /** * Determine approval requirements based on discount percentage */ -async function determineApprovalRequirements(ctx: TriggerContext): Promise { - const quote = ctx.new; +async function determineApprovalRequirements(ctx: any): Promise { + const quote = ctx.input?.doc || ctx.result; const discountPercent = quote.DiscountPercent || 0; try { @@ -122,8 +118,8 @@ async function determineApprovalRequirements(ctx: TriggerContext): Promise quote.ApprovalLevel = approvalLevel; // Set approval status to Pending if discount increased - if (ctx.old) { - const oldDiscount = ctx.old.DiscountPercent || 0; + if (ctx.previous) { + const oldDiscount = ctx.previous.DiscountPercent || 0; if (discountPercent > oldDiscount && quote.ApprovalStatus !== 'Approved' && quote.ApprovalStatus !== 'Pending') { quote.ApprovalStatus = 'Pending'; console.log(`๐Ÿ”„ Quote requires Level ${approvalLevel} approval (${(discountPercent * 100).toFixed(1)}% discount)`); @@ -141,8 +137,8 @@ async function determineApprovalRequirements(ctx: TriggerContext): Promise /** * Validate margin protection rules */ -async function validateMarginProtection(ctx: TriggerContext): Promise { - const quote = ctx.new; +async function validateMarginProtection(ctx: any): Promise { + const quote = ctx.input?.doc || ctx.result; try { // Calculate margin if we have cost information @@ -161,8 +157,8 @@ async function validateMarginProtection(ctx: TriggerContext): Promise { /** * Initialize quote after creation */ -async function initializeQuote(ctx: TriggerContext): Promise { - const quote = ctx.new; +async function initializeQuote(ctx: any): Promise { + const quote = ctx.result; try { // Log activity for new quote @@ -200,16 +196,16 @@ async function initializeQuote(ctx: TriggerContext): Promise { /** * Handle quote status changes */ -async function handleQuoteStatusChange(ctx: TriggerContext): Promise { - if (!ctx.old || !ctx.new) return; +async function handleQuoteStatusChange(ctx: any): Promise { + if (!ctx.previous || !ctx.result) return; - const statusChanged = ctx.old.Status !== ctx.new.Status; + const statusChanged = ctx.previous.Status !== ctx.result.Status; if (!statusChanged) return; - const quote = ctx.new; + const quote = ctx.result; try { - console.log(`๐Ÿ”„ Quote status changed from "${ctx.old.Status}" to "${quote.Status}"`); + console.log(`๐Ÿ”„ Quote status changed from "${ctx.previous.Status}" to "${quote.Status}"`); // Handle Accepted status if (quote.Status === 'Accepted') { @@ -229,7 +225,7 @@ async function handleQuoteStatusChange(ctx: TriggerContext): Promise { // Log activity for status change if (quote.AccountId) { await ctx.db.doc.create('Activity', { - Subject: `Quote Status Changed: ${ctx.old.Status} โ†’ ${quote.Status}`, + Subject: `Quote Status Changed: ${ctx.previous.Status} โ†’ ${quote.Status}`, Type: 'Quote Status Change', Status: 'Completed', Priority: 'Normal', @@ -237,7 +233,7 @@ async function handleQuoteStatusChange(ctx: TriggerContext): Promise { WhatId: quote.Id, OwnerId: ctx.user.id, ActivityDate: new Date().toISOString().split('T')[0], - Description: `Quote ${quote.QuoteNumber} status changed from "${ctx.old.Status}" to "${quote.Status}"` + Description: `Quote ${quote.QuoteNumber} status changed from "${ctx.previous.Status}" to "${quote.Status}"` }); } } catch (error) { @@ -248,8 +244,8 @@ async function handleQuoteStatusChange(ctx: TriggerContext): Promise { /** * Handle quote accepted */ -async function handleQuoteAccepted(ctx: TriggerContext): Promise { - const quote = ctx.new; +async function handleQuoteAccepted(ctx: any): Promise { + const quote = ctx.result; try { console.log('โœ… Processing quote acceptance...'); @@ -310,8 +306,8 @@ async function handleQuoteAccepted(ctx: TriggerContext): Promise { /** * Handle quote sent to customer */ -async function handleQuoteSent(ctx: TriggerContext): Promise { - const quote = ctx.new; +async function handleQuoteSent(ctx: any): Promise { + const quote = ctx.result; try { console.log('๐Ÿ“ง Quote sent to customer'); @@ -327,8 +323,8 @@ async function handleQuoteSent(ctx: TriggerContext): Promise { /** * Handle quote expiration */ -async function handleQuoteExpired(ctx: TriggerContext): Promise { - const quote = ctx.new; +async function handleQuoteExpired(ctx: any): Promise { + const quote = ctx.result; try { console.log('โฐ Quote expired'); @@ -344,16 +340,16 @@ async function handleQuoteExpired(ctx: TriggerContext): Promise { /** * Handle approval status changes */ -async function handleApprovalStatusChange(ctx: TriggerContext): Promise { - if (!ctx.old || !ctx.new) return; +async function handleApprovalStatusChange(ctx: any): Promise { + if (!ctx.previous || !ctx.result) return; - const approvalStatusChanged = ctx.old.ApprovalStatus !== ctx.new.ApprovalStatus; + const approvalStatusChanged = ctx.previous.ApprovalStatus !== ctx.result.ApprovalStatus; if (!approvalStatusChanged) return; - const quote = ctx.new; + const quote = ctx.result; try { - console.log(`๐Ÿ”„ Approval status changed from "${ctx.old.ApprovalStatus}" to "${quote.ApprovalStatus}"`); + console.log(`๐Ÿ”„ Approval status changed from "${ctx.previous.ApprovalStatus}" to "${quote.ApprovalStatus}"`); // Handle approved if (quote.ApprovalStatus === 'Approved') { diff --git a/packages/server/package.json b/packages/server/package.json index c90b3c7b0..81d79444f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,13 +18,13 @@ "@hotcrm/marketing": "workspace:*", "@hotcrm/products": "workspace:*", "@hotcrm/support": "workspace:*", - "@objectstack/cli": "^1.1.0", - "@objectstack/core": "1.1.0", - "@objectstack/metadata": "^1.1.0", - "@objectstack/objectql": "^1.1.0", - "@objectstack/plugin-hono-server": "1.1.0", - "@objectstack/runtime": "1.1.0", - "@objectstack/spec": "^1.1.0", + "@objectstack/cli": "^2.0.0", + "@objectstack/core": "2.0.0", + "@objectstack/metadata": "^2.0.0", + "@objectstack/objectql": "^2.0.0", + "@objectstack/plugin-hono-server": "2.0.0", + "@objectstack/runtime": "2.0.0", + "@objectstack/spec": "^2.0.0", "pino": "^10.3.0" }, "devDependencies": { diff --git a/packages/support/package.json b/packages/support/package.json index 94aa8c25b..b3f89a596 100644 --- a/packages/support/package.json +++ b/packages/support/package.json @@ -21,11 +21,11 @@ "author": "HotCRM Team", "license": "MIT", "dependencies": { - "@objectstack/spec": "^1.1.0", + "@objectstack/spec": "^2.0.0", "typescript": "^5.3.0" }, "peerDependencies": { - "@objectstack/cli": "^1.1.0" + "@objectstack/cli": "^2.0.0" }, "devDependencies": { "@types/node": "^25.2.0", diff --git a/packages/support/src/hooks/case.hook.ts b/packages/support/src/hooks/case.hook.ts index 47bcc7ad6..707f114d5 100644 --- a/packages/support/src/hooks/case.hook.ts +++ b/packages/support/src/hooks/case.hook.ts @@ -1,4 +1,4 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; // Ensure this points to the correct db instance /** @@ -11,8 +11,8 @@ const CaseEntitlementCheck: Hook = { name: 'CaseEntitlementCheck', object: 'case', // Lowercase per protocol events: ['beforeInsert'], - handler: async (ctx) => { - const caseRec = ctx.new; + handler: async (ctx: HookContext) => { + const caseRec = ctx.input.doc as Record; // Skip if no account is linked if (!caseRec.account) { diff --git a/packages/support/src/hooks/knowledge.hook.ts b/packages/support/src/hooks/knowledge.hook.ts index b2615dcf8..3b7385f21 100644 --- a/packages/support/src/hooks/knowledge.hook.ts +++ b/packages/support/src/hooks/knowledge.hook.ts @@ -1,13 +1,7 @@ -import type { Hook } from '@objectstack/spec/data'; +import type { Hook, HookContext } from '@objectstack/spec/data'; import { db } from '../db'; -// Types for Context -interface TriggerContext { - old?: Record; - new: Record; - db: typeof db; - user: { id: string; name: string; email: string; }; -} + /** * Knowledge Article Scoring Trigger @@ -21,17 +15,17 @@ const KnowledgeArticleScoringTrigger: Hook = { name: 'KnowledgeArticleScoringTrigger', object: 'KnowledgeArticle', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const article = ctx.new; - const oldArticle = ctx.old; + const article = ctx.input.doc as Record; + const oldArticle = ctx.previous as Record | undefined; // Calculate quality score article.QualityScore = calculateQualityScore(article); // Calculate popularity score (only if article has been published) if (article.Status === 'Published') { - article.PopularityScore = await calculatePopularityScore(article, ctx); + article.PopularityScore = await calculatePopularityScore(article); } // Update helpfulness rating @@ -63,10 +57,10 @@ const KnowledgeArticleAIEnhancementTrigger: Hook = { name: 'KnowledgeArticleAIEnhancementTrigger', object: 'KnowledgeArticle', events: ['beforeInsert', 'beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const article = ctx.new; - const oldArticle = ctx.old; + const article = ctx.input.doc as Record; + const oldArticle = ctx.previous as Record | undefined; // Only run AI on new articles or when content changes const isNew = !oldArticle; @@ -119,10 +113,10 @@ const KnowledgeArticleWorkflowTrigger: Hook = { name: 'KnowledgeArticleWorkflowTrigger', object: 'KnowledgeArticle', events: ['beforeUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const article = ctx.new; - const oldArticle = ctx.old; + const article = ctx.input.doc as Record; + const oldArticle = ctx.previous as Record | undefined; // Track status changes if (oldArticle && oldArticle.Status !== article.Status) { @@ -160,10 +154,10 @@ const KnowledgeArticleUsageTracker: Hook = { name: 'KnowledgeArticleUsageTracker', object: 'Case', events: ['afterUpdate'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { - const caseRecord = ctx.new; - const oldCase = ctx.old; + const caseRecord = ctx.result as Record; + const oldCase = ctx.previous as Record | undefined; // Track when case is resolved and knowledge was used if (oldCase && caseRecord.Status === 'Resolved' && oldCase.Status !== 'Resolved') { @@ -193,7 +187,7 @@ const KnowledgeArticleSearchAnalytics: Hook = { name: 'KnowledgeArticleSearchAnalytics', object: 'KnowledgeArticle', events: ['afterFind'], - handler: async (ctx: TriggerContext) => { + handler: async (ctx: HookContext) => { try { // Track search queries and results // This would be implemented at the search API level @@ -276,7 +270,7 @@ function calculateQualityScore(article: any): number { return Math.min(score, maxScore); } -async function calculatePopularityScore(article: any, ctx: TriggerContext): Promise { +async function calculatePopularityScore(article: any): Promise { let score = 0; const maxScore = 100; @@ -429,7 +423,7 @@ async function extractArticleKeywords(content: string): Promise { return sortedWords.join(', '); } -async function findRelatedArticles(article: any, ctx: TriggerContext): Promise { +async function findRelatedArticles(article: any, ctx: any): Promise { const keywords = article.AIKeywords?.split(',').map((k: string) => k.trim()) || []; if (keywords.length === 0) return []; @@ -446,7 +440,7 @@ async function findRelatedArticles(article: any, ctx: TriggerContext): Promise { +async function trackStatusChange(article: any, oldArticle: any, ctx: any): Promise { const statusChange = { ArticleId: article.id, OldStatus: oldArticle.Status, @@ -470,7 +464,7 @@ async function trackStatusChange(article: any, oldArticle: any, ctx: TriggerCont } } -async function checkForAutoArchive(article: any, ctx: TriggerContext): Promise { +async function checkForAutoArchive(article: any, ctx: any): Promise { // Auto-archive if: // 1. Article is older than 2 years // 2. Low views in last 6 months @@ -508,7 +502,7 @@ function calculateNextReviewDate(article: any): Date { return reviewDate; } -async function incrementArticleUsage(articleId: string, ctx: TriggerContext): Promise { +async function incrementArticleUsage(articleId: string, ctx: any): Promise { try { const article = await ctx.db.find('KnowledgeArticle', { filters: [['id', '=', articleId]], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b827227e5..3fdac3df7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: devDependencies: '@objectstack/cli': - specifier: ^1.1.0 - version: 1.1.0(esbuild@0.27.2) + specifier: ^2.0.0 + version: 2.0.0(esbuild@0.27.2) '@objectstack/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^2.0.0 + version: 2.0.0 '@types/node': specifier: ^25.2.0 version: 25.2.0 @@ -94,8 +94,8 @@ importers: packages/ai: dependencies: '@objectstack/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^2.0.0 + version: 2.0.0 axios: specifier: ^1.13.4 version: 1.13.4 @@ -125,8 +125,8 @@ importers: specifier: workspace:* version: link:../ai '@objectstack/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^2.0.0 + version: 2.0.0 axios: specifier: ^1.13.4 version: 1.13.4 @@ -153,8 +153,8 @@ importers: packages/finance: dependencies: '@objectstack/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^2.0.0 + version: 2.0.0 typescript: specifier: ^5.3.0 version: 5.9.3 @@ -175,8 +175,8 @@ importers: packages/hr: dependencies: '@objectstack/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^2.0.0 + version: 2.0.0 typescript: specifier: ^5.3.0 version: 5.9.3 @@ -197,8 +197,8 @@ importers: packages/marketing: dependencies: '@objectstack/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^2.0.0 + version: 2.0.0 devDependencies: typescript: specifier: ^5.0.0 @@ -207,8 +207,8 @@ importers: packages/products: dependencies: '@objectstack/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^2.0.0 + version: 2.0.0 typescript: specifier: ^5.3.0 version: 5.9.3 @@ -229,8 +229,8 @@ importers: packages/support: dependencies: '@objectstack/spec': - specifier: ^1.1.0 - version: 1.1.0 + specifier: ^2.0.0 + version: 2.0.0 typescript: specifier: ^5.3.0 version: 5.9.3 @@ -877,37 +877,34 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@objectstack/cli@1.1.0': - resolution: {integrity: sha512-yDsNPGwNC6SkGZopvlDzKDipcx7Wk7US4PKJ4SqLArDGrRRiY7gkW67rImR/0iO1bKGnEVurlKHKmemdIva8mA==} + '@objectstack/cli@2.0.0': + resolution: {integrity: sha512-xDV1gM435RTty/rtd8JuisJPty8Bmu3oPJlxA9RA1NMWKowEzw84NwLsm41cSPzaeEA2diRf+0oi+b1hNHNdMw==} hasBin: true - '@objectstack/core@1.1.0': - resolution: {integrity: sha512-BabYj3iESh5t5Yw7pwcRE+2gHZU2jl78/Emx9frUnZ9exkRurxGEynLXVK4Us5UQgjNjVJzZILAQPO5syTD33A==} + '@objectstack/core@2.0.0': + resolution: {integrity: sha512-8LdySGVAijOwPDznoxsAldpntvPbKahWy5A/8k3qYLt9FD4Y9Anx43nSBcFw3htA0YVIKtIPQrSI8JMjZoC3ew==} - '@objectstack/driver-memory@1.1.0': - resolution: {integrity: sha512-it1KUJDm1kWEZ50M1WXQKvcNnbm7Zbqwu1VKz5zn44/wbrPlxUQR2cYBICHPa/x1PvX9nQyGOUkx1BCnBHDgbw==} + '@objectstack/driver-memory@2.0.0': + resolution: {integrity: sha512-2oM0HKpP3RdRNus2b9LK83xLy1mQvXr/yDAaWVUk7bO7IBFUfl9IP075CBFxckLkTI+t83MyHGEo1Rg7xaKwwA==} - '@objectstack/hono@1.1.0': - resolution: {integrity: sha512-5uo9v5AsHJeP/nVsq3VBbezG2YKRzUHWPLK9LxbDJAmFldTGI10qbcZ/oAd+iPDZH+NYLdSN5mA9cX3GXqh79w==} - peerDependencies: - '@objectstack/runtime': 1.1.0 - hono: ^4.0.0 + '@objectstack/objectql@2.0.0': + resolution: {integrity: sha512-gb/2HB+ob0twHyofnfRnKBvIVwVRBy7S/EpVCAhJgvG67H117c1Yv03MvWecDwxRwCfzOpGybibrefdYRrHxjQ==} - '@objectstack/objectql@1.1.0': - resolution: {integrity: sha512-guV2hcN21jx0T6gJeXcppzXLmNS0Uu+ooaiN+knS4c9ZxPdTk2GnAyg2wlfL8kGCrYvOSm4y32zogZFd74snow==} + '@objectstack/plugin-hono-server@2.0.0': + resolution: {integrity: sha512-XUcDjzt8RYxxhDnyQA3qDrMKORWe0fNz97BYiLLYyw6CIT7l11k+42TAWpBJx2pa3JII19Vq6f13XLXTIN6Y/A==} - '@objectstack/plugin-hono-server@1.1.0': - resolution: {integrity: sha512-ZQfZUX1oFvYfuPT0JkCOCuFRONpTXthRsT1oxkvY/IVzrrDNBHij/uwXNYPg8/lVF8x5x/+Y0lhlIma+zqwJkQ==} + '@objectstack/rest@2.0.0': + resolution: {integrity: sha512-91lnlxiGADkFoiBV/A/jxpnB+aRoOlhuSg1gRx0I9INJM6Ar9jRjqWNKErsrgNXPVBtHNGM28hC9S2vX7jB8IQ==} - '@objectstack/runtime@1.1.0': - resolution: {integrity: sha512-6gcKCdUG4Y4OfUMbn6hWh5QGYooql7m7ejXXnKxmRrOV1M/a1CuW0KfTjiM/Dt+Qey0KIoemuwkyTQd1c0enbg==} + '@objectstack/runtime@2.0.0': + resolution: {integrity: sha512-uo6nzbErbC/alzolrD1n/u2jloosO7mJBOm+4VM3ZLtSQoa8i63tYPgak06dl1N53ydigjQ4aqv3S8Ob5wHmsw==} - '@objectstack/spec@1.1.0': - resolution: {integrity: sha512-U0NiY+8ByG4klzKcXyOWjE0IWie8Qykj3Pm/MYz2IGZB0kyZmL5GHSNUltmd18rs96Aqfst/vCTSKM8/nPmnUA==} + '@objectstack/spec@2.0.0': + resolution: {integrity: sha512-g0hoDiedEjta8wVe4G4trjGyvil4C/ryHZyQISiu0b3tAFQ4IEYRm5vxDKn87w7BFiT+7T9kbJcZYx7x/BUy+w==} engines: {node: '>=18.0.0'} - '@objectstack/types@1.1.0': - resolution: {integrity: sha512-akcqUcMX05SdikD300xX9CKFTC6OqAXtvpwmZ0IS4ZgZlt68Ljr4zXuqtPlf9l+E8f7l2xGG0XoPaehDmfdTYg==} + '@objectstack/types@2.0.0': + resolution: {integrity: sha512-7/ieuVYbqSN04G0+LiKxlmsMA0xAFhNhvpwLeu9pxbX9JerRKT9wwZK9VuS9guRfV0wQI/ZZONfE0tIYvMAQQQ==} '@orama/orama@3.1.18': resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} @@ -3844,14 +3841,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@objectstack/cli@1.1.0(esbuild@0.27.2)': + '@objectstack/cli@2.0.0(esbuild@0.27.2)': dependencies: - '@objectstack/core': 1.1.0 - '@objectstack/driver-memory': 1.1.0 - '@objectstack/objectql': 1.1.0 - '@objectstack/plugin-hono-server': 1.1.0 - '@objectstack/runtime': 1.1.0 - '@objectstack/spec': 1.1.0 + '@objectstack/core': 2.0.0 + '@objectstack/driver-memory': 2.0.0 + '@objectstack/objectql': 2.0.0 + '@objectstack/plugin-hono-server': 2.0.0 + '@objectstack/rest': 2.0.0 + '@objectstack/runtime': 2.0.0 + '@objectstack/spec': 2.0.0 bundle-require: 5.1.0(esbuild@0.27.2) chalk: 5.6.2 commander: 14.0.3 @@ -3860,53 +3858,52 @@ snapshots: transitivePeerDependencies: - esbuild - '@objectstack/core@1.1.0': + '@objectstack/core@2.0.0': dependencies: - '@objectstack/spec': 1.1.0 + '@objectstack/spec': 2.0.0 pino: 10.3.0 pino-pretty: 13.1.3 zod: 3.25.76 - '@objectstack/driver-memory@1.1.0': - dependencies: - '@objectstack/core': 1.1.0 - '@objectstack/spec': 1.1.0 - - '@objectstack/hono@1.1.0(@objectstack/runtime@1.1.0)(hono@4.11.7)': + '@objectstack/driver-memory@2.0.0': dependencies: - '@objectstack/runtime': 1.1.0 - hono: 4.11.7 + '@objectstack/core': 2.0.0 + '@objectstack/spec': 2.0.0 - '@objectstack/objectql@1.1.0': + '@objectstack/objectql@2.0.0': dependencies: - '@objectstack/core': 1.1.0 - '@objectstack/spec': 1.1.0 - '@objectstack/types': 1.1.0 + '@objectstack/core': 2.0.0 + '@objectstack/spec': 2.0.0 + '@objectstack/types': 2.0.0 - '@objectstack/plugin-hono-server@1.1.0': + '@objectstack/plugin-hono-server@2.0.0': dependencies: '@hono/node-server': 1.19.9(hono@4.11.7) - '@objectstack/core': 1.1.0 - '@objectstack/hono': 1.1.0(@objectstack/runtime@1.1.0)(hono@4.11.7) - '@objectstack/runtime': 1.1.0 - '@objectstack/spec': 1.1.0 - '@objectstack/types': 1.1.0 + '@objectstack/core': 2.0.0 + '@objectstack/spec': 2.0.0 hono: 4.11.7 - '@objectstack/runtime@1.1.0': + '@objectstack/rest@2.0.0': + dependencies: + '@objectstack/core': 2.0.0 + '@objectstack/spec': 2.0.0 + zod: 3.25.76 + + '@objectstack/runtime@2.0.0': dependencies: - '@objectstack/core': 1.1.0 - '@objectstack/spec': 1.1.0 - '@objectstack/types': 1.1.0 + '@objectstack/core': 2.0.0 + '@objectstack/rest': 2.0.0 + '@objectstack/spec': 2.0.0 + '@objectstack/types': 2.0.0 zod: 3.25.76 - '@objectstack/spec@1.1.0': + '@objectstack/spec@2.0.0': dependencies: zod: 3.25.76 - '@objectstack/types@1.1.0': + '@objectstack/types@2.0.0': dependencies: - '@objectstack/spec': 1.1.0 + '@objectstack/spec': 2.0.0 '@orama/orama@3.1.18': {}