@valentine-efagene/qshelter-common 2.0.21 → 2.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/generated/client/browser.d.ts +45 -30
  2. package/dist/generated/client/client.d.ts +45 -30
  3. package/dist/generated/client/commonInputTypes.d.ts +40 -0
  4. package/dist/generated/client/internal/class.d.ts +93 -60
  5. package/dist/generated/client/internal/class.js +2 -2
  6. package/dist/generated/client/internal/prismaNamespace.d.ts +1058 -720
  7. package/dist/generated/client/internal/prismaNamespace.js +321 -190
  8. package/dist/generated/client/internal/prismaNamespaceBrowser.d.ts +352 -215
  9. package/dist/generated/client/internal/prismaNamespaceBrowser.js +321 -190
  10. package/dist/generated/client/models/Amenity.d.ts +168 -1
  11. package/dist/generated/client/models/Contract.d.ts +2390 -309
  12. package/dist/generated/client/models/ContractDocument.d.ts +299 -12
  13. package/dist/generated/client/models/ContractEvent.d.ts +1052 -0
  14. package/dist/generated/client/models/ContractEvent.js +1 -0
  15. package/dist/generated/client/models/ContractInstallment.d.ts +1656 -0
  16. package/dist/generated/client/models/ContractInstallment.js +1 -0
  17. package/dist/generated/client/models/ContractPayment.d.ts +2026 -0
  18. package/dist/generated/client/models/ContractPayment.js +1 -0
  19. package/dist/generated/client/models/ContractPhase.d.ts +2467 -0
  20. package/dist/generated/client/models/ContractPhase.js +1 -0
  21. package/dist/generated/client/models/ContractPhaseStep.d.ts +1678 -0
  22. package/dist/generated/client/models/ContractPhaseStep.js +1 -0
  23. package/dist/generated/client/models/ContractPhaseStepApproval.d.ts +1249 -0
  24. package/dist/generated/client/models/ContractPhaseStepApproval.js +1 -0
  25. package/dist/generated/client/models/ContractTransition.d.ts +1118 -0
  26. package/dist/generated/client/models/ContractTransition.js +1 -0
  27. package/dist/generated/client/models/DomainEvent.d.ts +1240 -0
  28. package/dist/generated/client/models/DomainEvent.js +1 -0
  29. package/dist/generated/client/models/PaymentPlan.d.ts +467 -971
  30. package/dist/generated/client/models/Property.d.ts +372 -626
  31. package/dist/generated/client/models/PropertyPaymentMethod.d.ts +1714 -0
  32. package/dist/generated/client/models/PropertyPaymentMethod.js +1 -0
  33. package/dist/generated/client/models/PropertyPaymentMethodLink.d.ts +1158 -0
  34. package/dist/generated/client/models/PropertyPaymentMethodLink.js +1 -0
  35. package/dist/generated/client/models/PropertyPaymentMethodPhase.d.ts +1656 -0
  36. package/dist/generated/client/models/PropertyPaymentMethodPhase.js +1 -0
  37. package/dist/generated/client/models/PropertyUnit.d.ts +1598 -0
  38. package/dist/generated/client/models/PropertyUnit.js +1 -0
  39. package/dist/generated/client/models/PropertyVariant.d.ts +2079 -0
  40. package/dist/generated/client/models/PropertyVariant.js +1 -0
  41. package/dist/generated/client/models/PropertyVariantAmenity.d.ts +1080 -0
  42. package/dist/generated/client/models/PropertyVariantAmenity.js +1 -0
  43. package/dist/generated/client/models/PropertyVariantMedia.d.ts +1189 -0
  44. package/dist/generated/client/models/PropertyVariantMedia.js +1 -0
  45. package/dist/generated/client/models/Tenant.d.ts +482 -0
  46. package/dist/generated/client/models/User.d.ts +684 -427
  47. package/dist/generated/client/models/index.d.ts +15 -12
  48. package/dist/generated/client/models/index.js +15 -12
  49. package/dist/generated/client/models.d.ts +15 -12
  50. package/dist/src/index.d.ts +2 -0
  51. package/dist/src/index.js +2 -0
  52. package/dist/src/middleware/error-handler.d.ts +6 -0
  53. package/dist/src/middleware/error-handler.js +26 -0
  54. package/dist/src/middleware/index.d.ts +3 -0
  55. package/dist/src/middleware/index.js +3 -0
  56. package/dist/src/middleware/request-logger.d.ts +6 -0
  57. package/dist/src/middleware/request-logger.js +17 -0
  58. package/dist/src/middleware/tenant.d.ts +61 -0
  59. package/dist/src/middleware/tenant.js +85 -0
  60. package/dist/src/prisma/tenant.d.ts +51 -0
  61. package/dist/src/prisma/tenant.js +211 -0
  62. package/package.json +16 -2
  63. package/prisma/migrations/20251230104059_add_property_variants/migration.sql +622 -0
  64. package/prisma/migrations/20251230113413_add_multitenancy/migration.sql +54 -0
  65. package/prisma/schema.prisma +561 -267
@@ -1,28 +1,31 @@
1
1
  export * from './Amenity';
2
2
  export * from './Contract';
3
3
  export * from './ContractDocument';
4
+ export * from './ContractEvent';
5
+ export * from './ContractInstallment';
6
+ export * from './ContractPayment';
7
+ export * from './ContractPhase';
8
+ export * from './ContractPhaseStep';
9
+ export * from './ContractPhaseStepApproval';
10
+ export * from './ContractTransition';
4
11
  export * from './DeviceEndpoint';
12
+ export * from './DomainEvent';
5
13
  export * from './EmailPreference';
6
- export * from './Mortgage';
7
- export * from './MortgageDocument';
8
- export * from './MortgageDownpaymentInstallment';
9
- export * from './MortgageDownpaymentPayment';
10
- export * from './MortgageDownpaymentPlan';
11
- export * from './MortgageStep';
12
- export * from './MortgageTransition';
13
- export * from './MortgageTransitionEvent';
14
- export * from './MortgageType';
15
14
  export * from './OAuthState';
16
15
  export * from './PasswordReset';
17
- export * from './Payment';
18
- export * from './PaymentInstallment';
19
16
  export * from './PaymentPlan';
20
- export * from './PaymentSchedule';
21
17
  export * from './Permission';
22
18
  export * from './Property';
23
19
  export * from './PropertyAmenity';
24
20
  export * from './PropertyDocument';
25
21
  export * from './PropertyMedia';
22
+ export * from './PropertyPaymentMethod';
23
+ export * from './PropertyPaymentMethodLink';
24
+ export * from './PropertyPaymentMethodPhase';
25
+ export * from './PropertyUnit';
26
+ export * from './PropertyVariant';
27
+ export * from './PropertyVariantAmenity';
28
+ export * from './PropertyVariantMedia';
26
29
  export * from './RefreshToken';
27
30
  export * from './Role';
28
31
  export * from './RolePermission';
@@ -1,28 +1,31 @@
1
1
  export * from './Amenity';
2
2
  export * from './Contract';
3
3
  export * from './ContractDocument';
4
+ export * from './ContractEvent';
5
+ export * from './ContractInstallment';
6
+ export * from './ContractPayment';
7
+ export * from './ContractPhase';
8
+ export * from './ContractPhaseStep';
9
+ export * from './ContractPhaseStepApproval';
10
+ export * from './ContractTransition';
4
11
  export * from './DeviceEndpoint';
12
+ export * from './DomainEvent';
5
13
  export * from './EmailPreference';
6
- export * from './Mortgage';
7
- export * from './MortgageDocument';
8
- export * from './MortgageDownpaymentInstallment';
9
- export * from './MortgageDownpaymentPayment';
10
- export * from './MortgageDownpaymentPlan';
11
- export * from './MortgageStep';
12
- export * from './MortgageTransition';
13
- export * from './MortgageTransitionEvent';
14
- export * from './MortgageType';
15
14
  export * from './OAuthState';
16
15
  export * from './PasswordReset';
17
- export * from './Payment';
18
- export * from './PaymentInstallment';
19
16
  export * from './PaymentPlan';
20
- export * from './PaymentSchedule';
21
17
  export * from './Permission';
22
18
  export * from './Property';
23
19
  export * from './PropertyAmenity';
24
20
  export * from './PropertyDocument';
25
21
  export * from './PropertyMedia';
22
+ export * from './PropertyPaymentMethod';
23
+ export * from './PropertyPaymentMethodLink';
24
+ export * from './PropertyPaymentMethodPhase';
25
+ export * from './PropertyUnit';
26
+ export * from './PropertyVariant';
27
+ export * from './PropertyVariantAmenity';
28
+ export * from './PropertyVariantMedia';
26
29
  export * from './RefreshToken';
27
30
  export * from './Role';
28
31
  export * from './RolePermission';
@@ -18,20 +18,23 @@ export type * from './models/Property.js';
18
18
  export type * from './models/PropertyMedia.js';
19
19
  export type * from './models/PropertyDocument.js';
20
20
  export type * from './models/Amenity.js';
21
+ export type * from './models/PropertyVariant.js';
22
+ export type * from './models/PropertyVariantAmenity.js';
23
+ export type * from './models/PropertyVariantMedia.js';
24
+ export type * from './models/PropertyUnit.js';
21
25
  export type * from './models/PropertyAmenity.js';
22
- export type * from './models/Mortgage.js';
23
- export type * from './models/MortgageType.js';
24
- export type * from './models/MortgageDocument.js';
25
- export type * from './models/MortgageStep.js';
26
- export type * from './models/MortgageDownpaymentPlan.js';
27
- export type * from './models/MortgageDownpaymentInstallment.js';
28
- export type * from './models/MortgageDownpaymentPayment.js';
29
- export type * from './models/MortgageTransition.js';
30
- export type * from './models/MortgageTransitionEvent.js';
31
26
  export type * from './models/PaymentPlan.js';
32
- export type * from './models/PaymentSchedule.js';
33
- export type * from './models/PaymentInstallment.js';
34
- export type * from './models/Payment.js';
27
+ export type * from './models/PropertyPaymentMethod.js';
28
+ export type * from './models/PropertyPaymentMethodLink.js';
29
+ export type * from './models/PropertyPaymentMethodPhase.js';
35
30
  export type * from './models/Contract.js';
31
+ export type * from './models/ContractPhase.js';
32
+ export type * from './models/ContractPhaseStep.js';
33
+ export type * from './models/ContractPhaseStepApproval.js';
34
+ export type * from './models/ContractInstallment.js';
35
+ export type * from './models/ContractPayment.js';
36
36
  export type * from './models/ContractDocument.js';
37
+ export type * from './models/ContractTransition.js';
38
+ export type * from './models/ContractEvent.js';
39
+ export type * from './models/DomainEvent.js';
37
40
  export type * from './commonInputTypes.js';
@@ -4,3 +4,5 @@ export * from './config';
4
4
  export { PrismaClient } from '../generated/client/client';
5
5
  export * from '../generated/client/models';
6
6
  export * from './prisma/user';
7
+ export * from './prisma/tenant';
8
+ export * from './middleware';
package/dist/src/index.js CHANGED
@@ -4,3 +4,5 @@ export * from './config';
4
4
  export { PrismaClient } from '../generated/client/client';
5
5
  export * from '../generated/client/models';
6
6
  export * from './prisma/user';
7
+ export * from './prisma/tenant';
8
+ export * from './middleware';
@@ -0,0 +1,6 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ /**
3
+ * Global error handler middleware for Express applications.
4
+ * Handles ZodError, AppError, and generic errors with appropriate responses.
5
+ */
6
+ export declare function errorHandler(err: Error, req: Request, res: Response, next: NextFunction): Response<any, Record<string, any>>;
@@ -0,0 +1,26 @@
1
+ import { ZodError } from 'zod';
2
+ import { AppError } from '../utils/errors';
3
+ /**
4
+ * Global error handler middleware for Express applications.
5
+ * Handles ZodError, AppError, and generic errors with appropriate responses.
6
+ */
7
+ export function errorHandler(err, req, res, next) {
8
+ if (err instanceof ZodError) {
9
+ return res.status(400).json({
10
+ success: false,
11
+ error: 'Validation Error',
12
+ details: err.issues,
13
+ });
14
+ }
15
+ if (err instanceof AppError) {
16
+ return res.status(err.statusCode).json({
17
+ success: false,
18
+ error: err.message,
19
+ });
20
+ }
21
+ console.error('Unhandled error:', err);
22
+ return res.status(500).json({
23
+ success: false,
24
+ error: 'Internal Server Error',
25
+ });
26
+ }
@@ -0,0 +1,3 @@
1
+ export * from './error-handler';
2
+ export * from './request-logger';
3
+ export * from './tenant';
@@ -0,0 +1,3 @@
1
+ export * from './error-handler';
2
+ export * from './request-logger';
3
+ export * from './tenant';
@@ -0,0 +1,6 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ /**
3
+ * Request logging middleware that logs method, path, status code, and duration.
4
+ * Logs in JSON format for easy parsing by log aggregation tools.
5
+ */
6
+ export declare function requestLogger(req: Request, res: Response, next: NextFunction): void;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Request logging middleware that logs method, path, status code, and duration.
3
+ * Logs in JSON format for easy parsing by log aggregation tools.
4
+ */
5
+ export function requestLogger(req, res, next) {
6
+ const start = Date.now();
7
+ res.on('finish', () => {
8
+ const duration = Date.now() - start;
9
+ console.log(JSON.stringify({
10
+ method: req.method,
11
+ path: req.path,
12
+ statusCode: res.statusCode,
13
+ duration,
14
+ }));
15
+ });
16
+ next();
17
+ }
@@ -0,0 +1,61 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { PrismaClient } from '../../generated/client/client';
3
+ import { TenantContext, TenantPrismaClient } from '../prisma/tenant';
4
+ /**
5
+ * Extend Express Request to include tenant context and scoped Prisma client
6
+ */
7
+ declare global {
8
+ namespace Express {
9
+ interface Request {
10
+ tenantContext?: TenantContext;
11
+ tenantPrisma?: TenantPrismaClient | PrismaClient;
12
+ }
13
+ }
14
+ }
15
+ /**
16
+ * Options for tenant middleware
17
+ */
18
+ export interface TenantMiddlewareOptions {
19
+ /**
20
+ * The base Prisma client to use for creating tenant-scoped clients
21
+ */
22
+ prisma: PrismaClient;
23
+ /**
24
+ * Whether to create a tenant-scoped Prisma client automatically
25
+ * If false, only tenantContext is attached to the request
26
+ * @default true
27
+ */
28
+ createScopedClient?: boolean;
29
+ }
30
+ /**
31
+ * Creates a tenant middleware that extracts tenant context from the request
32
+ * and optionally creates a tenant-scoped Prisma client.
33
+ *
34
+ * The API Gateway authorizer is expected to set:
35
+ * - x-tenant-id: The tenant ID from the JWT
36
+ * - x-user-id: The user ID from the JWT
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { createTenantMiddleware } from '@valentine-efagene/qshelter-common';
41
+ * import { prisma } from './lib/prisma';
42
+ *
43
+ * app.use(createTenantMiddleware({ prisma }));
44
+ * ```
45
+ */
46
+ export declare function createTenantMiddleware(options: TenantMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
47
+ /**
48
+ * Middleware that requires tenant context.
49
+ * Use this for routes that must have tenant scoping.
50
+ */
51
+ export declare function requireTenant(req: Request, res: Response, next: NextFunction): void;
52
+ /**
53
+ * Helper to get tenant-scoped Prisma client from request.
54
+ * Throws if not available.
55
+ */
56
+ export declare function getTenantPrisma(req: Request): TenantPrismaClient | PrismaClient;
57
+ /**
58
+ * Helper to get tenant context from request.
59
+ * Throws if not available.
60
+ */
61
+ export declare function getTenantContext(req: Request): TenantContext;
@@ -0,0 +1,85 @@
1
+ import { AppError } from '../utils/errors';
2
+ import { createTenantPrisma, } from '../prisma/tenant';
3
+ /**
4
+ * Creates a tenant middleware that extracts tenant context from the request
5
+ * and optionally creates a tenant-scoped Prisma client.
6
+ *
7
+ * The API Gateway authorizer is expected to set:
8
+ * - x-tenant-id: The tenant ID from the JWT
9
+ * - x-user-id: The user ID from the JWT
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { createTenantMiddleware } from '@valentine-efagene/qshelter-common';
14
+ * import { prisma } from './lib/prisma';
15
+ *
16
+ * app.use(createTenantMiddleware({ prisma }));
17
+ * ```
18
+ */
19
+ export function createTenantMiddleware(options) {
20
+ const { prisma, createScopedClient = true } = options;
21
+ return function tenantMiddleware(req, res, next) {
22
+ try {
23
+ const headers = req.headers;
24
+ const tenantId = headers['x-tenant-id'];
25
+ const userId = headers['x-user-id'];
26
+ if (!tenantId) {
27
+ // For now, allow requests without tenant context for development
28
+ // In production, you might want to reject these
29
+ console.warn('Request without tenant context:', req.path);
30
+ return next();
31
+ }
32
+ const tenantContext = {
33
+ tenantId,
34
+ userId,
35
+ };
36
+ // Attach tenant context to request
37
+ req.tenantContext = tenantContext;
38
+ // Create tenant-scoped Prisma client if enabled
39
+ if (createScopedClient) {
40
+ req.tenantPrisma = createTenantPrisma(prisma, tenantContext);
41
+ }
42
+ else {
43
+ req.tenantPrisma = prisma;
44
+ }
45
+ next();
46
+ }
47
+ catch (error) {
48
+ console.error('Tenant middleware error:', error);
49
+ next(error);
50
+ }
51
+ };
52
+ }
53
+ /**
54
+ * Middleware that requires tenant context.
55
+ * Use this for routes that must have tenant scoping.
56
+ */
57
+ export function requireTenant(req, res, next) {
58
+ if (!req.tenantContext?.tenantId) {
59
+ return next(new AppError(400, 'Tenant context required'));
60
+ }
61
+ if (!req.tenantPrisma) {
62
+ return next(new AppError(500, 'Tenant Prisma client not initialized'));
63
+ }
64
+ next();
65
+ }
66
+ /**
67
+ * Helper to get tenant-scoped Prisma client from request.
68
+ * Throws if not available.
69
+ */
70
+ export function getTenantPrisma(req) {
71
+ if (!req.tenantPrisma) {
72
+ throw new AppError(500, 'Tenant context not available');
73
+ }
74
+ return req.tenantPrisma;
75
+ }
76
+ /**
77
+ * Helper to get tenant context from request.
78
+ * Throws if not available.
79
+ */
80
+ export function getTenantContext(req) {
81
+ if (!req.tenantContext) {
82
+ throw new AppError(500, 'Tenant context not available');
83
+ }
84
+ return req.tenantContext;
85
+ }
@@ -0,0 +1,51 @@
1
+ import { PrismaClient, Prisma } from "../../generated/client/client";
2
+ /**
3
+ * Tenant context for request-scoped operations
4
+ */
5
+ export interface TenantContext {
6
+ tenantId: string;
7
+ userId?: string;
8
+ }
9
+ /**
10
+ * Creates a tenant-scoped Prisma client that automatically:
11
+ * 1. Filters all queries on tenant-scoped models by tenantId
12
+ * 2. Injects tenantId into all create operations on tenant-scoped models
13
+ *
14
+ * Usage:
15
+ * ```ts
16
+ * const tenantPrisma = createTenantPrisma(prisma, { tenantId: 'tenant-123' });
17
+ * const properties = await tenantPrisma.property.findMany(); // Auto-filtered by tenant
18
+ * ```
19
+ */
20
+ export declare function createTenantPrisma(prisma: PrismaClient, context: TenantContext): import("@prisma/client/runtime/client").DynamicClientExtensionThis<Prisma.TypeMap<import("@prisma/client/runtime/client").InternalArgs & {
21
+ result: {};
22
+ model: {};
23
+ query: {};
24
+ client: {};
25
+ }, Prisma.GlobalOmitConfig | undefined>, Prisma.TypeMapCb<Prisma.GlobalOmitConfig | undefined>, {
26
+ result: {};
27
+ model: {};
28
+ query: {};
29
+ client: {};
30
+ }>;
31
+ /**
32
+ * Type helper for the tenant-scoped Prisma client
33
+ */
34
+ export type TenantPrismaClient = ReturnType<typeof createTenantPrisma>;
35
+ /**
36
+ * Helper to verify a record belongs to the current tenant
37
+ * Use this for operations that can't be filtered at query time
38
+ */
39
+ export declare function assertTenantOwnership<T extends {
40
+ tenantId?: string | null;
41
+ }>(record: T | null, tenantId: string, options?: {
42
+ allowGlobal?: boolean;
43
+ }): asserts record is T;
44
+ /**
45
+ * Utility to check if a record is accessible by a tenant
46
+ */
47
+ export declare function canAccessRecord(record: {
48
+ tenantId?: string | null;
49
+ } | null, tenantId: string, options?: {
50
+ allowGlobal?: boolean;
51
+ }): boolean;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Models that require tenant scoping
3
+ */
4
+ const TENANT_SCOPED_MODELS = [
5
+ "property",
6
+ "paymentPlan",
7
+ "propertyPaymentMethod",
8
+ "contract",
9
+ ];
10
+ /**
11
+ * Models that can optionally have tenant scoping (nullable tenantId)
12
+ * PaymentPlan has nullable tenantId for global templates
13
+ */
14
+ const OPTIONAL_TENANT_MODELS = ["paymentPlan"];
15
+ function isTenantScopedModel(model) {
16
+ return TENANT_SCOPED_MODELS.includes(model);
17
+ }
18
+ function isOptionalTenantModel(model) {
19
+ return OPTIONAL_TENANT_MODELS.includes(model);
20
+ }
21
+ /**
22
+ * Creates a tenant-scoped Prisma client that automatically:
23
+ * 1. Filters all queries on tenant-scoped models by tenantId
24
+ * 2. Injects tenantId into all create operations on tenant-scoped models
25
+ *
26
+ * Usage:
27
+ * ```ts
28
+ * const tenantPrisma = createTenantPrisma(prisma, { tenantId: 'tenant-123' });
29
+ * const properties = await tenantPrisma.property.findMany(); // Auto-filtered by tenant
30
+ * ```
31
+ */
32
+ export function createTenantPrisma(prisma, context) {
33
+ const { tenantId } = context;
34
+ return prisma.$extends({
35
+ name: "tenant-scoping",
36
+ query: {
37
+ $allModels: {
38
+ async findMany({ model, args, query }) {
39
+ if (isTenantScopedModel(model)) {
40
+ const tenantFilter = isOptionalTenantModel(model)
41
+ ? { OR: [{ tenantId }, { tenantId: null }] }
42
+ : { tenantId };
43
+ args.where = {
44
+ ...args.where,
45
+ ...tenantFilter,
46
+ };
47
+ }
48
+ return query(args);
49
+ },
50
+ async findFirst({ model, args, query }) {
51
+ if (isTenantScopedModel(model)) {
52
+ const tenantFilter = isOptionalTenantModel(model)
53
+ ? { OR: [{ tenantId }, { tenantId: null }] }
54
+ : { tenantId };
55
+ args.where = {
56
+ ...args.where,
57
+ ...tenantFilter,
58
+ };
59
+ }
60
+ return query(args);
61
+ },
62
+ async findUnique({ model, args, query }) {
63
+ // findUnique can only filter by unique fields, so we verify after fetch
64
+ const result = await query(args);
65
+ if (result && isTenantScopedModel(model)) {
66
+ const record = result;
67
+ if (isOptionalTenantModel(model)) {
68
+ // Allow null tenantId (global) or matching tenantId
69
+ if (record.tenantId !== null && record.tenantId !== tenantId) {
70
+ return null;
71
+ }
72
+ }
73
+ else {
74
+ if (record.tenantId !== tenantId) {
75
+ return null;
76
+ }
77
+ }
78
+ }
79
+ return result;
80
+ },
81
+ async create({ model, args, query }) {
82
+ if (isTenantScopedModel(model) && !isOptionalTenantModel(model)) {
83
+ // Inject tenantId for required tenant models
84
+ args.data.tenantId = tenantId;
85
+ }
86
+ else if (isOptionalTenantModel(model)) {
87
+ // For optional models, inject if not explicitly set to null
88
+ if (args.data.tenantId === undefined) {
89
+ args.data.tenantId = tenantId;
90
+ }
91
+ }
92
+ return query(args);
93
+ },
94
+ async createMany({ model, args, query }) {
95
+ if (isTenantScopedModel(model)) {
96
+ const data = Array.isArray(args.data) ? args.data : [args.data];
97
+ args.data = data.map((item) => {
98
+ if (!isOptionalTenantModel(model)) {
99
+ return { ...item, tenantId };
100
+ }
101
+ // For optional models, inject if not explicitly set
102
+ if (item.tenantId === undefined) {
103
+ return { ...item, tenantId };
104
+ }
105
+ return item;
106
+ });
107
+ }
108
+ return query(args);
109
+ },
110
+ async update({ model, args, query }) {
111
+ if (isTenantScopedModel(model)) {
112
+ // Verify tenant ownership before update
113
+ const tenantFilter = isOptionalTenantModel(model)
114
+ ? { OR: [{ tenantId }, { tenantId: null }] }
115
+ : { tenantId };
116
+ args.where = {
117
+ ...args.where,
118
+ ...tenantFilter,
119
+ };
120
+ }
121
+ return query(args);
122
+ },
123
+ async updateMany({ model, args, query }) {
124
+ if (isTenantScopedModel(model)) {
125
+ const tenantFilter = isOptionalTenantModel(model)
126
+ ? { OR: [{ tenantId }, { tenantId: null }] }
127
+ : { tenantId };
128
+ args.where = {
129
+ ...args.where,
130
+ ...tenantFilter,
131
+ };
132
+ }
133
+ return query(args);
134
+ },
135
+ async delete({ model, args, query }) {
136
+ if (isTenantScopedModel(model)) {
137
+ const tenantFilter = isOptionalTenantModel(model)
138
+ ? { OR: [{ tenantId }, { tenantId: null }] }
139
+ : { tenantId };
140
+ args.where = {
141
+ ...args.where,
142
+ ...tenantFilter,
143
+ };
144
+ }
145
+ return query(args);
146
+ },
147
+ async deleteMany({ model, args, query }) {
148
+ if (isTenantScopedModel(model)) {
149
+ const tenantFilter = isOptionalTenantModel(model)
150
+ ? { OR: [{ tenantId }, { tenantId: null }] }
151
+ : { tenantId };
152
+ args.where = {
153
+ ...args.where,
154
+ ...tenantFilter,
155
+ };
156
+ }
157
+ return query(args);
158
+ },
159
+ async count({ model, args, query }) {
160
+ if (isTenantScopedModel(model)) {
161
+ const tenantFilter = isOptionalTenantModel(model)
162
+ ? { OR: [{ tenantId }, { tenantId: null }] }
163
+ : { tenantId };
164
+ args.where = {
165
+ ...args.where,
166
+ ...tenantFilter,
167
+ };
168
+ }
169
+ return query(args);
170
+ },
171
+ async aggregate({ model, args, query }) {
172
+ if (isTenantScopedModel(model)) {
173
+ const tenantFilter = isOptionalTenantModel(model)
174
+ ? { OR: [{ tenantId }, { tenantId: null }] }
175
+ : { tenantId };
176
+ args.where = {
177
+ ...args.where,
178
+ ...tenantFilter,
179
+ };
180
+ }
181
+ return query(args);
182
+ },
183
+ },
184
+ },
185
+ });
186
+ }
187
+ /**
188
+ * Helper to verify a record belongs to the current tenant
189
+ * Use this for operations that can't be filtered at query time
190
+ */
191
+ export function assertTenantOwnership(record, tenantId, options) {
192
+ if (!record) {
193
+ throw new Error("Record not found");
194
+ }
195
+ if (options?.allowGlobal && record.tenantId === null) {
196
+ return; // Global records are accessible to all tenants
197
+ }
198
+ if (record.tenantId !== tenantId) {
199
+ throw new Error("Access denied: resource belongs to another tenant");
200
+ }
201
+ }
202
+ /**
203
+ * Utility to check if a record is accessible by a tenant
204
+ */
205
+ export function canAccessRecord(record, tenantId, options) {
206
+ if (!record)
207
+ return false;
208
+ if (options?.allowGlobal && record.tenantId === null)
209
+ return true;
210
+ return record.tenantId === tenantId;
211
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentine-efagene/qshelter-common",
3
- "version": "2.0.21",
3
+ "version": "2.0.24",
4
4
  "description": "Shared database schemas and utilities for QShelter services",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -34,8 +34,22 @@
34
34
  "dotenv": "^17.2.3",
35
35
  "prisma": "^7.0.0"
36
36
  },
37
+ "peerDependencies": {
38
+ "express": "^4.0.0 || ^5.0.0",
39
+ "zod": "^3.0.0 || ^4.0.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "express": {
43
+ "optional": true
44
+ },
45
+ "zod": {
46
+ "optional": true
47
+ }
48
+ },
37
49
  "devDependencies": {
50
+ "@types/express": "^5.0.0",
38
51
  "@types/node": "^25.0.3",
39
- "typescript": "^5.7.3"
52
+ "typescript": "^5.7.3",
53
+ "zod": "^4.0.0"
40
54
  }
41
55
  }