tychat-contracts 1.0.87 → 1.0.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/event-analytic.enum.d.ts +1 -1
- package/dist/analytics/event-analytic.enum.d.ts.map +1 -1
- package/dist/analytics/event-analytic.enum.js +10 -3
- package/dist/analytics/followup-analytic-event-type.util.d.ts +12 -0
- package/dist/analytics/followup-analytic-event-type.util.d.ts.map +1 -0
- package/dist/analytics/followup-analytic-event-type.util.js +24 -0
- package/dist/analytics/index.d.ts +1 -0
- package/dist/analytics/index.d.ts.map +1 -1
- package/dist/analytics/index.js +1 -0
- package/package.json +27 -27
- package/src/analytics/event-analytic.enum.ts +10 -3
- package/src/analytics/followup-analytic-event-type.util.ts +44 -0
- package/src/analytics/index.ts +1 -0
- package/src/tenants/tenant-slug.util.ts +56 -56
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Enum of all analytic event types emitted by microservices.
|
|
3
3
|
* Each value follows the pattern: `domain.action`.
|
|
4
4
|
*/
|
|
5
|
-
export declare const EVENT_ANALYTIC_TYPES: readonly ["patient.created", "patient.updated", "patient.deleted", "patient.status_changed", "appointment.created", "appointment.updated", "appointment.canceled", "appointment.rescheduled", "appointment.no_show", "appointment.finished", "checkin.effectued", "checkout.effectued", "message.sent", "message.received", "conversation.session_started", "conversation.session_ended", "conversation.transferred_to_human", "followup.
|
|
5
|
+
export declare const EVENT_ANALYTIC_TYPES: readonly ["patient.created", "patient.updated", "patient.deleted", "patient.status_changed", "appointment.created", "appointment.updated", "appointment.canceled", "appointment.rescheduled", "appointment.no_show", "appointment.finished", "checkin.effectued", "checkout.effectued", "message.sent", "message.received", "conversation.session_started", "conversation.session_ended", "conversation.transferred_to_human", "followup.abandonment.sent", "followup.appointment_confirmation.sent", "followup.satisfaction_booking.sent", "followup.satisfaction_finished.sent", "followup.no_show_reschedule.sent", "followup.wellness_check.sent", "followup.return_suggestion.sent", "followup.satisfaction_booking.response_received", "followup.satisfaction_finished.response_received", "followup.config_updated", "payment.created", "payment.payed", "payment.refunded", "payment.canceled", "payment.overdue", "payment_gateway.payment_created", "payment_gateway.payment_confirmed", "payment_gateway.payment_failed", "payment_gateway.payment_refunded", "payment_gateway.payment_canceled", "payment_gateway.payment_expired", "payment_gateway.webhook_received", "billing.invoice_created", "billing.invoice_paid", "ai.interaction", "ai.fallback", "auth.login", "auth.logout", "auth.password_reset", "user.created", "user.updated", "user.deleted", "notification.sent", "notification.failed", "whatsapp.connection_established", "whatsapp.connection_lost", "procedure.created", "procedure.updated", "procedure.deleted", "specialty.created", "specialty.updated", "specialty.deleted", "professional.created", "professional.updated", "professional.deleted", "configuration.updated", "storage.file_uploaded", "storage.file_deleted", "tenant.created", "tenant.updated"];
|
|
6
6
|
export type EventAnalyticType = (typeof EVENT_ANALYTIC_TYPES)[number];
|
|
7
7
|
//# sourceMappingURL=event-analytic.enum.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-analytic.enum.d.ts","sourceRoot":"","sources":["../../src/analytics/event-analytic.enum.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"event-analytic.enum.d.ts","sourceRoot":"","sources":["../../src/analytics/event-analytic.enum.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,4sDA2GvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -28,9 +28,16 @@ exports.EVENT_ANALYTIC_TYPES = [
|
|
|
28
28
|
'conversation.session_started',
|
|
29
29
|
'conversation.session_ended',
|
|
30
30
|
'conversation.transferred_to_human',
|
|
31
|
-
// ── Follow-up events
|
|
32
|
-
'followup.
|
|
33
|
-
'followup.
|
|
31
|
+
// ── Follow-up events (tychat-followup-service) ─────────────────
|
|
32
|
+
'followup.abandonment.sent',
|
|
33
|
+
'followup.appointment_confirmation.sent',
|
|
34
|
+
'followup.satisfaction_booking.sent',
|
|
35
|
+
'followup.satisfaction_finished.sent',
|
|
36
|
+
'followup.no_show_reschedule.sent',
|
|
37
|
+
'followup.wellness_check.sent',
|
|
38
|
+
'followup.return_suggestion.sent',
|
|
39
|
+
'followup.satisfaction_booking.response_received',
|
|
40
|
+
'followup.satisfaction_finished.response_received',
|
|
34
41
|
'followup.config_updated',
|
|
35
42
|
// ── Payment events ──────────────────────────────────────────────
|
|
36
43
|
'payment.created',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EventAnalyticType } from './event-analytic.enum';
|
|
2
|
+
/**
|
|
3
|
+
* Tipos de follow-up baseados em agendamento (fila `followup-appointment`),
|
|
4
|
+
* alinhados a {@link FOLLOWUP_TYPES} exceto `abandonment`.
|
|
5
|
+
*/
|
|
6
|
+
export type FollowupAppointmentDispatchKind = 'appointment_confirmation' | 'satisfaction_booking' | 'satisfaction_finished' | 'no_show_reschedule' | 'wellness_check' | 'return_suggestion';
|
|
7
|
+
export declare function followupAppointmentSentEventType(kind: FollowupAppointmentDispatchKind): EventAnalyticType;
|
|
8
|
+
export type FollowupSatisfactionSurveyKind = 'satisfaction_booking' | 'satisfaction_finished';
|
|
9
|
+
export declare function followupSatisfactionResponseEventType(satisfactionType: FollowupSatisfactionSurveyKind): EventAnalyticType;
|
|
10
|
+
/** Evento quando um passo de abandono é enviado com sucesso. */
|
|
11
|
+
export declare const FOLLOWUP_ABANDONMENT_SENT_EVENT: EventAnalyticType;
|
|
12
|
+
//# sourceMappingURL=followup-analytic-event-type.util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"followup-analytic-event-type.util.d.ts","sourceRoot":"","sources":["../../src/analytics/followup-analytic-event-type.util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D;;;GAGG;AACH,MAAM,MAAM,+BAA+B,GACvC,0BAA0B,GAC1B,sBAAsB,GACtB,uBAAuB,GACvB,oBAAoB,GACpB,gBAAgB,GAChB,mBAAmB,CAAC;AAWxB,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,+BAA+B,GACpC,iBAAiB,CAEnB;AAED,MAAM,MAAM,8BAA8B,GACtC,sBAAsB,GACtB,uBAAuB,CAAC;AAE5B,wBAAgB,qCAAqC,CACnD,gBAAgB,EAAE,8BAA8B,GAC/C,iBAAiB,CAKnB;AAED,gEAAgE;AAChE,eAAO,MAAM,+BAA+B,EAAE,iBAA+C,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FOLLOWUP_ABANDONMENT_SENT_EVENT = void 0;
|
|
4
|
+
exports.followupAppointmentSentEventType = followupAppointmentSentEventType;
|
|
5
|
+
exports.followupSatisfactionResponseEventType = followupSatisfactionResponseEventType;
|
|
6
|
+
const APPOINTMENT_SENT = {
|
|
7
|
+
appointment_confirmation: 'followup.appointment_confirmation.sent',
|
|
8
|
+
satisfaction_booking: 'followup.satisfaction_booking.sent',
|
|
9
|
+
satisfaction_finished: 'followup.satisfaction_finished.sent',
|
|
10
|
+
no_show_reschedule: 'followup.no_show_reschedule.sent',
|
|
11
|
+
wellness_check: 'followup.wellness_check.sent',
|
|
12
|
+
return_suggestion: 'followup.return_suggestion.sent',
|
|
13
|
+
};
|
|
14
|
+
function followupAppointmentSentEventType(kind) {
|
|
15
|
+
return APPOINTMENT_SENT[kind];
|
|
16
|
+
}
|
|
17
|
+
function followupSatisfactionResponseEventType(satisfactionType) {
|
|
18
|
+
if (satisfactionType === 'satisfaction_booking') {
|
|
19
|
+
return 'followup.satisfaction_booking.response_received';
|
|
20
|
+
}
|
|
21
|
+
return 'followup.satisfaction_finished.response_received';
|
|
22
|
+
}
|
|
23
|
+
/** Evento quando um passo de abandono é enviado com sucesso. */
|
|
24
|
+
exports.FOLLOWUP_ABANDONMENT_SENT_EVENT = 'followup.abandonment.sent';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analytics/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analytics/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,qCAAqC,CAAC;AACpD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC"}
|
package/dist/analytics/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./event-analytic.enum"), exports);
|
|
18
|
+
__exportStar(require("./followup-analytic-event-type.util"), exports);
|
|
18
19
|
__exportStar(require("./create-analytic-event.dto"), exports);
|
|
19
20
|
__exportStar(require("./analytics-query.dto"), exports);
|
|
20
21
|
__exportStar(require("./analytics-kafka-topics"), exports);
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "tychat-contracts",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "DTOs compartilhados com class-validator (API e microserviços)",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"private": false,
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build": "tsc",
|
|
10
|
-
"prepublishOnly": "npm run build"
|
|
11
|
-
},
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"class-transformer": "^0.5.1",
|
|
14
|
-
"class-validator": "^0.14.1",
|
|
15
|
-
"ioredis": "^5.10.0",
|
|
16
|
-
"jest": "^30.3.0",
|
|
17
|
-
"reflect-metadata": "*"
|
|
18
|
-
},
|
|
19
|
-
"devDependencies": {
|
|
20
|
-
"@nestjs/swagger": "^11.2.6",
|
|
21
|
-
"typescript": "^5.7.3"
|
|
22
|
-
},
|
|
23
|
-
"peerDependencies": {
|
|
24
|
-
"@nestjs/swagger": "^11.2.6",
|
|
25
|
-
"reflect-metadata": "*"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "tychat-contracts",
|
|
3
|
+
"version": "1.0.89",
|
|
4
|
+
"description": "DTOs compartilhados com class-validator (API e microserviços)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"private": false,
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"class-transformer": "^0.5.1",
|
|
14
|
+
"class-validator": "^0.14.1",
|
|
15
|
+
"ioredis": "^5.10.0",
|
|
16
|
+
"jest": "^30.3.0",
|
|
17
|
+
"reflect-metadata": "*"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@nestjs/swagger": "^11.2.6",
|
|
21
|
+
"typescript": "^5.7.3"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@nestjs/swagger": "^11.2.6",
|
|
25
|
+
"reflect-metadata": "*"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -30,9 +30,16 @@ export const EVENT_ANALYTIC_TYPES = [
|
|
|
30
30
|
'conversation.session_ended',
|
|
31
31
|
'conversation.transferred_to_human',
|
|
32
32
|
|
|
33
|
-
// ── Follow-up events
|
|
34
|
-
'followup.
|
|
35
|
-
'followup.
|
|
33
|
+
// ── Follow-up events (tychat-followup-service) ─────────────────
|
|
34
|
+
'followup.abandonment.sent',
|
|
35
|
+
'followup.appointment_confirmation.sent',
|
|
36
|
+
'followup.satisfaction_booking.sent',
|
|
37
|
+
'followup.satisfaction_finished.sent',
|
|
38
|
+
'followup.no_show_reschedule.sent',
|
|
39
|
+
'followup.wellness_check.sent',
|
|
40
|
+
'followup.return_suggestion.sent',
|
|
41
|
+
'followup.satisfaction_booking.response_received',
|
|
42
|
+
'followup.satisfaction_finished.response_received',
|
|
36
43
|
'followup.config_updated',
|
|
37
44
|
|
|
38
45
|
// ── Payment events ──────────────────────────────────────────────
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { EventAnalyticType } from './event-analytic.enum';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tipos de follow-up baseados em agendamento (fila `followup-appointment`),
|
|
5
|
+
* alinhados a {@link FOLLOWUP_TYPES} exceto `abandonment`.
|
|
6
|
+
*/
|
|
7
|
+
export type FollowupAppointmentDispatchKind =
|
|
8
|
+
| 'appointment_confirmation'
|
|
9
|
+
| 'satisfaction_booking'
|
|
10
|
+
| 'satisfaction_finished'
|
|
11
|
+
| 'no_show_reschedule'
|
|
12
|
+
| 'wellness_check'
|
|
13
|
+
| 'return_suggestion';
|
|
14
|
+
|
|
15
|
+
const APPOINTMENT_SENT: Record<FollowupAppointmentDispatchKind, EventAnalyticType> = {
|
|
16
|
+
appointment_confirmation: 'followup.appointment_confirmation.sent',
|
|
17
|
+
satisfaction_booking: 'followup.satisfaction_booking.sent',
|
|
18
|
+
satisfaction_finished: 'followup.satisfaction_finished.sent',
|
|
19
|
+
no_show_reschedule: 'followup.no_show_reschedule.sent',
|
|
20
|
+
wellness_check: 'followup.wellness_check.sent',
|
|
21
|
+
return_suggestion: 'followup.return_suggestion.sent',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function followupAppointmentSentEventType(
|
|
25
|
+
kind: FollowupAppointmentDispatchKind,
|
|
26
|
+
): EventAnalyticType {
|
|
27
|
+
return APPOINTMENT_SENT[kind];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type FollowupSatisfactionSurveyKind =
|
|
31
|
+
| 'satisfaction_booking'
|
|
32
|
+
| 'satisfaction_finished';
|
|
33
|
+
|
|
34
|
+
export function followupSatisfactionResponseEventType(
|
|
35
|
+
satisfactionType: FollowupSatisfactionSurveyKind,
|
|
36
|
+
): EventAnalyticType {
|
|
37
|
+
if (satisfactionType === 'satisfaction_booking') {
|
|
38
|
+
return 'followup.satisfaction_booking.response_received';
|
|
39
|
+
}
|
|
40
|
+
return 'followup.satisfaction_finished.response_received';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Evento quando um passo de abandono é enviado com sucesso. */
|
|
44
|
+
export const FOLLOWUP_ABANDONMENT_SENT_EVENT: EventAnalyticType = 'followup.abandonment.sent';
|
package/src/analytics/index.ts
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility functions for tenant slug <-> domain transformations.
|
|
3
|
-
*
|
|
4
|
-
* Convention:
|
|
5
|
-
* - Slugs stored in DB use underscores for word separation (e.g. "clinica_sampaio")
|
|
6
|
-
* or are single words (e.g. "homolog"). Hyphens are NOT allowed in slugs.
|
|
7
|
-
* - DNS hostnames use hyphens (underscores are invalid in DNS labels).
|
|
8
|
-
* - Conversion is bijective: slug "clinica_sampaio" <-> domain "clinica-sampaio"
|
|
9
|
-
*
|
|
10
|
-
* Examples:
|
|
11
|
-
* - Slug "homolog" -> domain segment "homolog" -> slug "homolog"
|
|
12
|
-
* - Slug "clinica_sampaio" -> domain segment "clinica-sampaio" -> slug "clinica_sampaio"
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Converts a tenant slug to a DNS-safe domain segment.
|
|
17
|
-
* Replaces underscores with hyphens and lowercases the result.
|
|
18
|
-
*
|
|
19
|
-
* @example slugToDomainSegment("clinica_sampaio") // "clinica-sampaio"
|
|
20
|
-
* @example slugToDomainSegment("homolog") // "homolog"
|
|
21
|
-
*/
|
|
22
|
-
export function slugToDomainSegment(slug: string): string {
|
|
23
|
-
return (slug || 'default').toLowerCase().replace(/_/g, '-');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Converts a domain segment back to the tenant slug.
|
|
28
|
-
* Replaces hyphens with underscores (the stored DB format).
|
|
29
|
-
*
|
|
30
|
-
* @example domainSegmentToSlug("clinica-sampaio") // "clinica_sampaio"
|
|
31
|
-
* @example domainSegmentToSlug("homolog") // "homolog"
|
|
32
|
-
*/
|
|
33
|
-
export function domainSegmentToSlug(domainSegment: string): string {
|
|
34
|
-
return (domainSegment || 'default').toLowerCase().replace(/-/g, '_');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Regex pattern that validates an acceptable tenant slug.
|
|
39
|
-
* Allows lowercase alphanumeric characters and underscores only (no hyphens).
|
|
40
|
-
* Must start and end with an alphanumeric character.
|
|
41
|
-
* Single-character slugs (e.g. "a") are allowed.
|
|
42
|
-
* Max 63 chars (DNS label limit after _ -> - conversion).
|
|
43
|
-
*/
|
|
44
|
-
export const TENANT_SLUG_REGEX = /^[a-z0-9]([a-z0-9_]*[a-z0-9])?$/;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Validates whether a slug is acceptable.
|
|
48
|
-
* Rules:
|
|
49
|
-
* - Only lowercase letters, digits, and underscores (no hyphens)
|
|
50
|
-
* - Must start and end with a letter or digit
|
|
51
|
-
* - Length between 1 and 63 characters
|
|
52
|
-
*/
|
|
53
|
-
export function isValidTenantSlug(slug: string): boolean {
|
|
54
|
-
if (!slug || slug.length > 63) return false;
|
|
55
|
-
return TENANT_SLUG_REGEX.test(slug.toLowerCase());
|
|
56
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for tenant slug <-> domain transformations.
|
|
3
|
+
*
|
|
4
|
+
* Convention:
|
|
5
|
+
* - Slugs stored in DB use underscores for word separation (e.g. "clinica_sampaio")
|
|
6
|
+
* or are single words (e.g. "homolog"). Hyphens are NOT allowed in slugs.
|
|
7
|
+
* - DNS hostnames use hyphens (underscores are invalid in DNS labels).
|
|
8
|
+
* - Conversion is bijective: slug "clinica_sampaio" <-> domain "clinica-sampaio"
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* - Slug "homolog" -> domain segment "homolog" -> slug "homolog"
|
|
12
|
+
* - Slug "clinica_sampaio" -> domain segment "clinica-sampaio" -> slug "clinica_sampaio"
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a tenant slug to a DNS-safe domain segment.
|
|
17
|
+
* Replaces underscores with hyphens and lowercases the result.
|
|
18
|
+
*
|
|
19
|
+
* @example slugToDomainSegment("clinica_sampaio") // "clinica-sampaio"
|
|
20
|
+
* @example slugToDomainSegment("homolog") // "homolog"
|
|
21
|
+
*/
|
|
22
|
+
export function slugToDomainSegment(slug: string): string {
|
|
23
|
+
return (slug || 'default').toLowerCase().replace(/_/g, '-');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Converts a domain segment back to the tenant slug.
|
|
28
|
+
* Replaces hyphens with underscores (the stored DB format).
|
|
29
|
+
*
|
|
30
|
+
* @example domainSegmentToSlug("clinica-sampaio") // "clinica_sampaio"
|
|
31
|
+
* @example domainSegmentToSlug("homolog") // "homolog"
|
|
32
|
+
*/
|
|
33
|
+
export function domainSegmentToSlug(domainSegment: string): string {
|
|
34
|
+
return (domainSegment || 'default').toLowerCase().replace(/-/g, '_');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Regex pattern that validates an acceptable tenant slug.
|
|
39
|
+
* Allows lowercase alphanumeric characters and underscores only (no hyphens).
|
|
40
|
+
* Must start and end with an alphanumeric character.
|
|
41
|
+
* Single-character slugs (e.g. "a") are allowed.
|
|
42
|
+
* Max 63 chars (DNS label limit after _ -> - conversion).
|
|
43
|
+
*/
|
|
44
|
+
export const TENANT_SLUG_REGEX = /^[a-z0-9]([a-z0-9_]*[a-z0-9])?$/;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates whether a slug is acceptable.
|
|
48
|
+
* Rules:
|
|
49
|
+
* - Only lowercase letters, digits, and underscores (no hyphens)
|
|
50
|
+
* - Must start and end with a letter or digit
|
|
51
|
+
* - Length between 1 and 63 characters
|
|
52
|
+
*/
|
|
53
|
+
export function isValidTenantSlug(slug: string): boolean {
|
|
54
|
+
if (!slug || slug.length > 63) return false;
|
|
55
|
+
return TENANT_SLUG_REGEX.test(slug.toLowerCase());
|
|
56
|
+
}
|