@praxium/sdk 0.3.36 → 0.3.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,13 +11,12 @@ npm install @praxium/sdk
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
- import { createTenantClient } from '@praxium/sdk'
14
+ import { createPraxiumClient } from '@praxium/sdk'
15
15
 
16
- const client = createTenantClient({
16
+ const client = createPraxiumClient({
17
17
  baseUrl: process.env.PRAXIUM_API_URL!, // 'https://platform.praxium.nl'
18
- apiKey: process.env.PRAXIUM_API_KEY!, // HMAC key: 'hmac_v1_mypractice_1234567890_abc...'
19
- tenantSlug: 'mypractice', // must match the slug in the API key
20
- locale: 'en', // 'en' (English) or 'nl' (Dutch)
18
+ apiKey: process.env.PRAXIUM_API_KEY!, // tenant slug extracted from key automatically
19
+ locale: 'nl', // 'nl' | 'en' | '*' (multilingual)
21
20
  })
22
21
 
23
22
  // Returns data directly — throws PraxiumError on failure
@@ -25,6 +24,32 @@ const hours = await client.getOpeningHours()
25
24
  const team = await client.getTeamMembers()
26
25
  ```
27
26
 
27
+ ### Multilingual Mode
28
+
29
+ ```typescript
30
+ import { createPraxiumClient, localizeText } from '@praxium/sdk'
31
+
32
+ const client = createPraxiumClient({
33
+ baseUrl: process.env.PRAXIUM_API_URL!,
34
+ apiKey: process.env.PRAXIUM_API_KEY!,
35
+ locale: '*', // returns all translations
36
+ })
37
+
38
+ const team = await client.getTeamMembers()
39
+ // Returns MultilingualTeamMember[] — labels/values are { nl: "...", en: "..." } objects:
40
+ // {
41
+ // name: "Dr. Jan de Vries",
42
+ // role: "Fysiotherapeut",
43
+ // customFields: [
44
+ // { label: { nl: "Specialisatie", en: "Specialization" }, value: { nl: "Sport", en: "Sports" } }
45
+ // ]
46
+ // }
47
+
48
+ // Resolve multilingual text to a specific locale at render time
49
+ const label = localizeText(team[0].customFields[0].label, 'nl') // → "Specialisatie"
50
+ const value = localizeText(team[0].customFields[0].value, 'nl') // → "Sport"
51
+ ```
52
+
28
53
  ## Next.js Revalidation
29
54
 
30
55
  Optional utilities for ISR cache revalidation (requires `next` as peer dependency):
package/dist/index.d.ts CHANGED
@@ -180,20 +180,37 @@ type SocialLinks = {
180
180
  */
181
181
  type TeamMembers = Array<PublicTeamMember>;
182
182
  /**
183
- * Custom field exposed on the public team API
183
+ * Custom field with server-resolved label (specific locale requested).
184
+ * When locale is 'nl' or 'en', the server resolves labels and values
185
+ * to that locale — label is a plain string, values are resolved strings.
184
186
  */
185
187
  type CustomField = {
186
188
  /** Field identifier (snake_case) */
187
189
  identifier: string;
188
190
  /** Custom field type (e.g., STRING, LONG_TEXT, SEARCHABLE_LIST) */
189
191
  type: string;
190
- /** Localized field label */
191
- label: LocalizedText;
192
+ /** Server-resolved field label for the requested locale */
193
+ label: string;
192
194
  /** Resolved field value (type depends on field.type) */
193
195
  value: unknown;
194
196
  };
195
197
  /**
196
- * Public team member profile
198
+ * Custom field with multilingual label (locale='*' requested).
199
+ * When locale is '*', the server returns all translations — label
200
+ * is a MultilingualText object, values are multilingual objects.
201
+ */
202
+ type MultilingualCustomField = {
203
+ /** Field identifier (snake_case) */
204
+ identifier: string;
205
+ /** Custom field type (e.g., STRING, LONG_TEXT, SEARCHABLE_LIST) */
206
+ type: string;
207
+ /** Multilingual field label (all available locales) */
208
+ label: MultilingualText;
209
+ /** Multilingual field value (type depends on field.type) */
210
+ value: unknown;
211
+ };
212
+ /**
213
+ * Public team member profile with server-resolved custom fields
197
214
  */
198
215
  type PublicTeamMember = {
199
216
  /**
@@ -209,10 +226,33 @@ type PublicTeamMember = {
209
226
  */
210
227
  publicImage: string;
211
228
  /**
212
- * Custom fields assigned to the public-team API access profile
229
+ * Custom fields with server-resolved labels and values
213
230
  */
214
231
  customFields: Array<CustomField>;
215
232
  };
233
+ /**
234
+ * Public team member profile with multilingual custom fields (locale='*')
235
+ */
236
+ type MultilingualTeamMember = {
237
+ /**
238
+ * Staff member UUID
239
+ */
240
+ id: string;
241
+ /**
242
+ * Public display name
243
+ */
244
+ publicName: string;
245
+ /**
246
+ * Public profile image URL (Supabase CDN)
247
+ */
248
+ publicImage: string;
249
+ /**
250
+ * Custom fields with multilingual labels and values
251
+ */
252
+ customFields: Array<MultilingualCustomField>;
253
+ };
254
+ /** Text in multiple languages. Keys are locale codes (e.g., 'nl', 'en'). */
255
+ type MultilingualText = Record<string, string>;
216
256
  /**
217
257
  * Bilingual text with Dutch (nl) and English (en) translations
218
258
  */
@@ -226,8 +266,6 @@ type BilingualText = {
226
266
  */
227
267
  en: string;
228
268
  };
229
- /** Text with locale-specific values. Keys are locale codes (e.g., 'nl', 'en'). */
230
- type LocalizedText = Record<string, string>;
231
269
  /**
232
270
  * FAQs grouped by category, ordered by category.order then faq.order
233
271
  */
@@ -611,25 +649,41 @@ type SubmitContactFormData = {
611
649
  url: '/api/public/{tenantSlug}/contact';
612
650
  };
613
651
 
614
- interface TenantClientConfig {
652
+ /**
653
+ * Supported locale codes for server-side content resolution.
654
+ * Adding a new language requires an SDK release.
655
+ */
656
+ type SupportedLocale = 'nl' | 'en';
657
+ /**
658
+ * Locale parameter for client creation.
659
+ * - Specific locale (`'nl'`, `'en'`) — server resolves labels/values
660
+ * - `'*'` — server returns all translations (multilingual mode)
661
+ */
662
+ type ClientLocale = SupportedLocale | '*';
663
+ /**
664
+ * Extract the tenant slug from a profile-scoped API key.
665
+ *
666
+ * Key format: hmac_v1_{tenantSlug}_{profileSlug}_{timestamp}_{signature}
667
+ * Parts: [0]hmac [1]v1 [2]tenantSlug [3]profileSlug [4]timestamp [5]signature
668
+ *
669
+ * @throws Error if key format is invalid (wrong prefix or insufficient parts)
670
+ */
671
+ declare function extractTenantSlugFromApiKey(apiKey: string): string;
672
+ interface PraxiumClientConfig {
615
673
  /** Platform base URL (e.g., 'https://platform.praxium.nl') */
616
674
  baseUrl: string;
617
- /** HMAC-signed API key (format: hmac_v1_{slug}_{timestamp}_{signature}) */
675
+ /** HMAC-signed API key (format: hmac_v1_{tenantSlug}_{profileSlug}_{timestamp}_{signature}) */
618
676
  apiKey: string;
619
- /** Tenant identifier slug (must match the slug in the API key) */
620
- tenantSlug: string;
621
- /** Accept-Language header for locale-aware responses (e.g., 'nl', 'en') */
622
- locale: string;
677
+ /** Locale for server-side content resolution */
678
+ locale: ClientLocale;
623
679
  }
624
- declare function createTenantClient(config: TenantClientConfig): {
625
- /** Resolve a LocalizedText to a string using the client's configured locale. */
626
- localize: (text: LocalizedText | null | undefined) => string;
680
+ /** Shared methods available on both client types */
681
+ interface PraxiumClientBase {
627
682
  getOpeningHours: () => Promise<OpeningHours>;
628
683
  getContactDetails: () => Promise<ContactDetails>;
629
684
  getLocation: () => Promise<Location>;
630
685
  getBusinessName: () => Promise<BusinessName>;
631
686
  getSocialLinks: () => Promise<SocialLinks>;
632
- getTeamMembers: () => Promise<TeamMembers>;
633
687
  getFaq: () => Promise<FaqContent>;
634
688
  getServiceVariants: () => Promise<PricingVariants>;
635
689
  getInsuranceInfo: () => Promise<InsuranceList>;
@@ -637,15 +691,27 @@ declare function createTenantClient(config: TenantClientConfig): {
637
691
  getPaymentMethods: () => Promise<PaymentMethodList>;
638
692
  getPolicyInfo: () => Promise<PolicyList>;
639
693
  getBookableServices: () => Promise<BookableServices>;
640
- submitContactForm: (body: SubmitContactFormData["body"]) => Promise<ContactFormResult>;
641
- };
642
- /** Return type of createTenantClient for type inference */
643
- type TenantClient = ReturnType<typeof createTenantClient>;
694
+ submitContactForm: (body: SubmitContactFormData['body']) => Promise<ContactFormResult>;
695
+ }
696
+ /** Client for locale-specific responses (locale='nl', 'en', etc.) */
697
+ interface PraxiumClient extends PraxiumClientBase {
698
+ getTeamMembers: () => Promise<TeamMembers>;
699
+ }
700
+ /** Client for multilingual responses (locale='*') */
701
+ interface MultilingualPraxiumClient extends PraxiumClientBase {
702
+ getTeamMembers: () => Promise<MultilingualTeamMember[]>;
703
+ }
704
+ /** Multilingual mode: returns MultilingualTeamMember[] (use localizeText() helper for resolution) */
705
+ declare function createPraxiumClient(config: PraxiumClientConfig & {
706
+ locale: '*';
707
+ }): MultilingualPraxiumClient;
708
+ /** Locale-specific mode: returns PublicTeamMember[] with resolved labels/values */
709
+ declare function createPraxiumClient(config: PraxiumClientConfig): PraxiumClient;
644
710
 
645
711
  /**
646
712
  * Typed error hierarchy for the Praxium SDK.
647
713
  *
648
- * Every method on `TenantClient` throws one of these errors when the
714
+ * Every method on `PraxiumClient` throws one of these errors when the
649
715
  * API returns a non-2xx response. Consumers can catch a specific
650
716
  * subclass (e.g. `PraxiumNotFoundError`) or the base `PraxiumError`.
651
717
  *
@@ -707,39 +773,69 @@ declare class PraxiumRateLimitError extends PraxiumError {
707
773
  /**
708
774
  * Get a custom field value by identifier from a team member's custom fields.
709
775
  *
776
+ * Works with both resolved (locale-specific) and multilingual (locale='*') members.
777
+ *
710
778
  * **Type safety warning:** The generic `T` is a cast (`as T`) — the caller is
711
779
  * responsible for ensuring `T` matches the actual runtime value shape for the
712
780
  * given field type. No runtime validation is performed.
713
781
  *
714
- * Runtime value shapes by custom field type:
782
+ * Runtime value shapes by custom field type (locale-specific):
715
783
  * - `STRING` → `string`
716
784
  * - `LONG_TEXT` → `string`
717
785
  * - `NUMBER` → `number`
718
786
  * - `BOOLEAN` → `boolean`
719
- * - `SINGLE_SELECT` → `BilingualText` (`{ nl: string; en: string }`)
720
- * - `MULTI_SELECT` → `BilingualText[]`
721
- * - `SEARCHABLE_LIST` → `BilingualText[]`
787
+ * - `SINGLE_SELECT` → `string`
788
+ * - `MULTI_SELECT` → `string[]`
789
+ * - `SEARCHABLE_LIST` → `string[]`
722
790
  * - `DATE` → `string` (ISO 8601 date)
723
791
  *
792
+ * Runtime value shapes by custom field type (locale='*'):
793
+ * - `SINGLE_SELECT` → `MultilingualText`
794
+ * - `MULTI_SELECT` → `MultilingualText[]`
795
+ * - `SEARCHABLE_LIST` → `MultilingualText[]`
796
+ * - Others: same as locale-specific
797
+ *
724
798
  * @example
725
799
  * ```ts
726
- * // Caller must match T to the field's actual type:
727
- * const specs = getCustomFieldValue<BilingualText[]>(member, 'specializations')
800
+ * // Locale-specific client (values are resolved strings):
801
+ * const specs = getCustomFieldValue<string[]>(member, 'specializations')
728
802
  * const bigNr = getCustomFieldValue<string>(member, 'big_number')
803
+ *
804
+ * // Multilingual client (locale='*'):
805
+ * const specs = getCustomFieldValue<MultilingualText[]>(member, 'specializations')
729
806
  * ```
730
807
  *
731
- * @param member - Public team member
808
+ * @param member - Public team member (resolved or multilingual)
732
809
  * @param identifier - Field identifier to look up
733
810
  * @returns The field value cast to `T`, or `null` if no field with that identifier exists
734
811
  */
735
- declare function getCustomFieldValue<T = unknown>(member: PublicTeamMember, identifier: string): T | null;
812
+ declare function getCustomFieldValue<T = unknown>(member: PublicTeamMember | MultilingualTeamMember, identifier: string): T | null;
736
813
  /**
737
814
  * Get a custom field by identifier.
738
815
  *
739
- * @param member - Public team member
816
+ * @param member - Public team member (resolved or multilingual)
740
817
  * @param identifier - Field identifier to look up
741
818
  * @returns The full custom field, or undefined if not found
742
819
  */
743
820
  declare function getCustomField(member: PublicTeamMember, identifier: string): CustomField | undefined;
821
+ declare function getCustomField(member: MultilingualTeamMember, identifier: string): MultilingualCustomField | undefined;
822
+ /**
823
+ * Resolve a MultilingualText object to a single string for the given locale.
824
+ *
825
+ * Fallback chain: requested locale → 'en' (fallback) → first available value → ''
826
+ *
827
+ * @param text - Multilingual text object with locale keys
828
+ * @param locale - Desired locale code (e.g., 'nl', 'en', 'de')
829
+ * @returns The resolved text string, or empty string if no value available
830
+ *
831
+ * @example
832
+ * ```typescript
833
+ * const label = { nl: 'Specialisaties', en: 'Specializations' }
834
+ * localizeText(label, 'nl') // 'Specialisaties'
835
+ * localizeText(label, 'de') // 'Specializations' (falls back to 'en')
836
+ * localizeText({}, 'nl') // ''
837
+ * ```
838
+ */
839
+ declare function localizeText(text: MultilingualText | null | undefined, locale: SupportedLocale): string;
744
840
 
745
- export { type ApiError, type BilingualText, type BookableService, type BookableServices, type BookableVariantInfo, type BusinessName, type ContactDetails, type ContactFormResult, type CustomField, type FaqCategory, type FaqContent, type FaqGroup, type FaqItem, type FeatureItem, type FeatureList, type InsuranceInfo, type InsuranceList, type LocalizedText, type Location, type OpeningHours, type PaymentMethod, type PaymentMethodList, type PolicyInfo, type PolicyList, PraxiumAuthError, PraxiumError, PraxiumForbiddenError, PraxiumNotFoundError, PraxiumRateLimitError, PraxiumValidationError, type PricingVariant, type PricingVariants, type PublicDaySchedule, type PublicTeamMember, type ServiceCategoryInfo, type SocialLinks, type TeamMembers, type TenantClient, type TenantClientConfig, type ValidationDetail, createTenantClient, getCustomField, getCustomFieldValue };
841
+ export { type ApiError, type BilingualText, type BookableService, type BookableServices, type BookableVariantInfo, type BusinessName, type ClientLocale, type ContactDetails, type ContactFormResult, type CustomField, type FaqCategory, type FaqContent, type FaqGroup, type FaqItem, type FeatureItem, type FeatureList, type InsuranceInfo, type InsuranceList, type Location, type MultilingualCustomField, type MultilingualPraxiumClient, type MultilingualTeamMember, type MultilingualText, type OpeningHours, type PaymentMethod, type PaymentMethodList, type PolicyInfo, type PolicyList, PraxiumAuthError, type PraxiumClient, type PraxiumClientConfig, PraxiumError, PraxiumForbiddenError, PraxiumNotFoundError, PraxiumRateLimitError, PraxiumValidationError, type PricingVariant, type PricingVariants, type PublicDaySchedule, type PublicTeamMember, type ServiceCategoryInfo, type SocialLinks, type SupportedLocale, type TeamMembers, type ValidationDetail, createPraxiumClient, extractTenantSlugFromApiKey, getCustomField, getCustomFieldValue, localizeText };
package/dist/index.js CHANGED
@@ -928,20 +928,20 @@ var STATUS_ERROR_MAP = {
928
928
  429: PraxiumRateLimitError
929
929
  };
930
930
 
931
- // src/helpers.ts
932
- function getCustomFieldValue(member, identifier) {
933
- const field = member.customFields.find((f) => f.identifier === identifier);
934
- return field ? field.value : null;
935
- }
936
- function getCustomField(member, identifier) {
937
- return member.customFields.find((f) => f.identifier === identifier);
938
- }
939
- function getLocalizedText(text, locale) {
940
- if (!text) return "";
941
- return text[locale] || text["en"] || Object.values(text)[0] || "";
942
- }
943
-
944
931
  // src/tenant-client.ts
932
+ var API_KEY_PREFIX = "hmac_v1";
933
+ var API_KEY_SEPARATOR = "_";
934
+ function extractTenantSlugFromApiKey(apiKey) {
935
+ const parts = apiKey.split(API_KEY_SEPARATOR);
936
+ if (parts.length < 6 || `${parts[0]}${API_KEY_SEPARATOR}${parts[1]}` !== API_KEY_PREFIX) {
937
+ throw new PraxiumError(
938
+ 400,
939
+ "INVALID_API_KEY_FORMAT",
940
+ `API key must start with '${API_KEY_PREFIX}' and contain at least 6 segments. Format: hmac_v1_{tenantSlug}_{profileSlug}_{timestamp}_{signature}`
941
+ );
942
+ }
943
+ return parts[2];
944
+ }
945
945
  function unwrapOrThrow(result) {
946
946
  if (result.data !== void 0 && result.error === void 0) {
947
947
  return result.data.data;
@@ -957,7 +957,8 @@ function unwrapOrThrow(result) {
957
957
  }
958
958
  throw new PraxiumError(status, errorCode, message, details);
959
959
  }
960
- function createTenantClient(config) {
960
+ function createPraxiumClient(config) {
961
+ const tenantSlug = extractTenantSlugFromApiKey(config.apiKey);
961
962
  const clientInstance = createClient(
962
963
  createConfig({
963
964
  baseUrl: config.baseUrl,
@@ -968,11 +969,8 @@ function createTenantClient(config) {
968
969
  request.headers.set("Accept-Language", config.locale);
969
970
  return request;
970
971
  });
971
- const path = { tenantSlug: config.tenantSlug };
972
- return {
973
- // ─── Locale Helper ───
974
- /** Resolve a LocalizedText to a string using the client's configured locale. */
975
- localize: (text) => getLocalizedText(text, config.locale),
972
+ const path = { tenantSlug };
973
+ const baseMethods = {
976
974
  // ─── Layout Endpoints ───
977
975
  getOpeningHours: () => getOpeningHours({ client: clientInstance, path }).then(unwrapOrThrow),
978
976
  getContactDetails: () => getContactDetails({ client: clientInstance, path }).then(unwrapOrThrow),
@@ -980,7 +978,6 @@ function createTenantClient(config) {
980
978
  getBusinessName: () => getBusinessName({ client: clientInstance, path }).then(unwrapOrThrow),
981
979
  getSocialLinks: () => getSocialLinks({ client: clientInstance, path }).then(unwrapOrThrow),
982
980
  // ─── Content Endpoints ───
983
- getTeamMembers: () => getTeamMembers({ client: clientInstance, path }).then(unwrapOrThrow),
984
981
  getFaq: () => getFaq({ client: clientInstance, path }).then(unwrapOrThrow),
985
982
  getServiceVariants: () => getServiceVariants({ client: clientInstance, path }).then(unwrapOrThrow),
986
983
  getInsuranceInfo: () => getInsuranceInfo({ client: clientInstance, path }).then(unwrapOrThrow),
@@ -991,6 +988,29 @@ function createTenantClient(config) {
991
988
  // ─── Contact Form ───
992
989
  submitContactForm: (body) => submitContactForm({ client: clientInstance, path, body }).then(unwrapOrThrow)
993
990
  };
991
+ if (config.locale === "*") {
992
+ return {
993
+ ...baseMethods,
994
+ getTeamMembers: () => getTeamMembers({ client: clientInstance, path }).then(unwrapOrThrow)
995
+ };
996
+ }
997
+ return {
998
+ ...baseMethods,
999
+ getTeamMembers: () => getTeamMembers({ client: clientInstance, path }).then(unwrapOrThrow)
1000
+ };
1001
+ }
1002
+
1003
+ // src/helpers.ts
1004
+ function getCustomFieldValue(member, identifier) {
1005
+ const field = member.customFields.find((f) => f.identifier === identifier);
1006
+ return field ? field.value : null;
1007
+ }
1008
+ function getCustomField(member, identifier) {
1009
+ return member.customFields.find((f) => f.identifier === identifier);
1010
+ }
1011
+ function localizeText(text, locale) {
1012
+ if (!text) return "";
1013
+ return text[locale] || text["en"] || Object.values(text)[0] || "";
994
1014
  }
995
1015
  export {
996
1016
  PraxiumAuthError,
@@ -999,7 +1019,9 @@ export {
999
1019
  PraxiumNotFoundError,
1000
1020
  PraxiumRateLimitError,
1001
1021
  PraxiumValidationError,
1002
- createTenantClient,
1022
+ createPraxiumClient,
1023
+ extractTenantSlugFromApiKey,
1003
1024
  getCustomField,
1004
- getCustomFieldValue
1025
+ getCustomFieldValue,
1026
+ localizeText
1005
1027
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxium/sdk",
3
- "version": "0.3.36",
3
+ "version": "0.3.48",
4
4
  "description": "Official TypeScript SDK for the Praxium platform API",
5
5
  "type": "module",
6
6
  "exports": {