@econneq/gql-auth 2.0.1 → 2.0.2

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
@@ -1,304 +1,304 @@
1
- # @econneq/gql-auth
2
-
3
- GraphQL + Auth utilities for **multi-tenant** and **single-tenant** Next.js apps.
4
-
5
- Handles:
6
- - Server-side (internal Docker network) and client-side (browser proxy) GraphQL
7
- - Multi-tenant header injection (`X-Tenant-ID`, `X-Process-ID`, `X-Widget-ID`)
8
- - JWT auth on both server and client
9
- - File uploads (GraphQL multipart spec)
10
- - Batch mutations with per-item error reporting
11
-
12
- ---
13
-
14
- ## Installation
15
-
16
- ```bash
17
- npm install @econneq/gql-auth
18
- ```
19
-
20
- ### Peer dependencies
21
-
22
- ```bash
23
- npm install @apollo/client graphql next react
24
- ```
25
-
26
- ---
27
-
28
- ## Environment Variables
29
-
30
- | Variable | Side | Description |
31
- |---|---|---|
32
- | `DOMAIN_URL` | server | Base domain, e.g. `e-conneq.com` |
33
- | `PROTOCOL` | server | e.g. `https://` |
34
- | `BACKEND_URL_INT` | server | Internal Docker URL, e.g. `api:8000` |
35
- | `BACKEND_URL_EXT` | server | External backend host, e.g. `admin.e-conneq.com` |
36
- | `NEXT_PUBLIC_DOMAIN_URL` | both | Same as DOMAIN_URL (client-accessible) |
37
- | `NEXT_PUBLIC_PROTOCOL` | both | Same as PROTOCOL (client-accessible) |
38
- | `NEXT_PUBLIC_BACKEND_URL_EXT` | both | Same as BACKEND_URL_EXT (client-accessible) |
39
- | `API_KEY` | server | Optional service-to-service key |
40
-
41
- > The internal URL (`BACKEND_URL_INT`) is **never** exposed to the browser.
42
- > Client-side mutations route through Next.js `/api/graphql` proxy.
43
-
44
- ---
45
-
46
- ## Config Object
47
-
48
- Every function accepts a `config` object describing your tenants.
49
-
50
- ```ts
51
- // config.ts (shared across your project)
52
- import type { ConfigType } from "@econneq/gql-auth";
53
-
54
- export const config: ConfigType = {
55
- Subdomains: [
56
- { id: 1, subdomain: "acme", processId: "proc-1", widgetID: "wgt-1" },
57
- { id: 2, subdomain: "globex", processId: "proc-2" },
58
- ],
59
- };
60
- ```
61
-
62
- **Single-tenant**: put one entry in `Subdomains`. The library will always use it
63
- without requiring a `domain` parameter.
64
-
65
- ---
66
-
67
- ## Quick start
68
-
69
- ### 1. Register the cookie helper (Next.js app)
70
-
71
- In your root layout (`app/layout.tsx`) or instrumentation file:
72
-
73
- ```ts
74
- import { cookies } from "next/headers";
75
- import { registerCookiesFn } from "@econneq/gql-auth";
76
-
77
- registerCookiesFn(cookies);
78
- ```
79
-
80
- This lets the Apollo client forward cookies server-side without a hard
81
- `next/headers` import inside the package.
82
-
83
- ---
84
-
85
- ### 2. Server-side query (Server Component / Route Handler)
86
-
87
- ```ts
88
- import { queryServerGraphQL } from "@econneq/gql-auth";
89
- import { config } from "@/config";
90
- import { GET_USERS } from "@/graphql/queries";
91
-
92
- // Multi-tenant
93
- const data = await queryServerGraphQL({
94
- query: GET_USERS,
95
- variables: { active: true },
96
- domain: "acme", // ← matches Subdomains[].subdomain
97
- config,
98
- });
99
-
100
- // Single-tenant (omit domain)
101
- const data = await queryServerGraphQL({
102
- query: GET_USERS,
103
- config,
104
- });
105
- ```
106
-
107
- ### 3. Server-side mutation (Server Action)
108
-
109
- ```ts
110
- "use server";
111
- import { mutateServerGraphQL } from "@econneq/gql-auth";
112
-
113
- const result = await mutateServerGraphQL({
114
- query: CREATE_USER_MUTATION,
115
- variables: { input: formData },
116
- domain: params.domain,
117
- config,
118
- });
119
- ```
120
-
121
- ---
122
-
123
- ### 4. Client-side mutation with ApiFactory
124
-
125
- ```tsx
126
- "use client";
127
- import { ApiFactory, getToken } from "@econneq/gql-auth";
128
- import { useRouter } from "next/navigation";
129
- import { config } from "@/config";
130
-
131
- export function MyForm({ params }: { params: { domain: string } }) {
132
- const router = useRouter();
133
-
134
- const handleSubmit = async (formData: any) => {
135
- await ApiFactory({
136
- newData: formData,
137
- mutationName: "createUser",
138
- modelName: "user",
139
- successField: "username",
140
- query: CREATE_USER_MUTATION,
141
- params: { domain: params.domain }, // omit for single-tenant
142
- config,
143
- router,
144
- token: getToken() ?? undefined,
145
- onAlert: ({ title, status }) => {
146
- // Connect to your toast/alert system
147
- console.log(title, status);
148
- },
149
- redirect: true,
150
- redirectPath: "/dashboard",
151
- });
152
- };
153
-
154
- return <form onSubmit={...}>...</form>;
155
- }
156
- ```
157
-
158
- #### Batch mutations
159
-
160
- Pass an array to `newData`:
161
-
162
- ```ts
163
- await ApiFactory({
164
- newData: [item1, item2, item3],
165
- mutationName: "createProduct",
166
- modelName: "product",
167
- // ...
168
- });
169
- ```
170
-
171
- #### File uploads
172
-
173
- ```ts
174
- await ApiFactory({
175
- newData: formData,
176
- getFileMap: (item) => ({ avatar: item.avatarFile }),
177
- // ...
178
- });
179
- ```
180
-
181
- #### Return data instead of alert
182
-
183
- ```ts
184
- const user = await ApiFactory<{ id: string; username: string }>({
185
- returnResponseObject: true,
186
- // ...
187
- });
188
- ```
189
-
190
- ---
191
-
192
- ### 5. Direct upload mutation
193
-
194
- For full control over multipart uploads:
195
-
196
- ```ts
197
- import { uploadGraphQLMutation } from "@econneq/gql-auth";
198
-
199
- const response = await uploadGraphQLMutation({
200
- query: UPLOAD_DOCUMENT_MUTATION.loc!.source.body,
201
- variables: { title: "Report", file: null },
202
- fileMap: { file: selectedFile },
203
- params: { domain: "acme" },
204
- config,
205
- token: userToken,
206
- });
207
- ```
208
-
209
- ---
210
-
211
- ### 6. Auth utilities
212
-
213
- #### Client-side
214
-
215
- ```ts
216
- import { getToken, getUser, logout } from "@econneq/gql-auth";
217
-
218
- const token = getToken(); // cookie → localStorage
219
- const user = getUser(); // decoded JwtPayload
220
- logout(); // clears cookies + localStorage
221
- ```
222
-
223
- #### Server-side
224
-
225
- ```ts
226
- import {
227
- getServerToken,
228
- getServerUser,
229
- getPermissions,
230
- } from "@econneq/gql-auth";
231
-
232
- const token = await getServerToken();
233
- const user = await getServerUser();
234
-
235
- // Does the user have any access to module 42?
236
- const canRead = getPermissions(user?.permissions ?? [], 42);
237
-
238
- // Does the user have CREATE access to module 42?
239
- const canCreate = getPermissions(user?.permissions ?? [], 42, "C");
240
- ```
241
-
242
- ---
243
-
244
- ### 7. Apollo client (advanced)
245
-
246
- Direct access when you need a custom query/mutation outside ApiFactory:
247
-
248
- ```ts
249
- import { getApolloClient } from "@econneq/gql-auth";
250
-
251
- const client = await getApolloClient({
252
- domain: "acme", // omit for single-tenant
253
- config,
254
- });
255
-
256
- const { data } = await client.query({ query: MY_QUERY });
257
- ```
258
-
259
- ---
260
-
261
- ## Architecture
262
-
263
- ```
264
- Browser Next.js Server Backend (Docker)
265
- ───────── ────────────── ────────────────
266
- uploadGraphQLMutation ──► /api/graphql (proxy) ──► api:8000/graphql/
267
- ApiFactory ──► /api/graphql (proxy) ──► api:8000/graphql/
268
-
269
- queryServerGraphQL ────────────────────────────► http://api:8000/graphql/
270
- mutateServerGraphQL ────────────────────────────► http://api:8000/graphql/
271
- getApolloClient ────────────────────────────► http://api:8000/graphql/
272
- ```
273
-
274
- Server-side calls skip the proxy entirely and hit the backend directly
275
- through Docker's internal network — faster and no public exposure.
276
-
277
- ---
278
-
279
- ## Permissions format
280
-
281
- Permission strings encode resource + action:
282
-
283
- ```
284
- "42" → access resource 42 (any action)
285
- "42C" → create on resource 42
286
- "42R" → read on resource 42
287
- "42U" → update on resource 42
288
- "42D" → delete on resource 42
289
- ```
290
-
291
- ---
292
-
293
- ## Changelog
294
-
295
- ### 2.0.0
296
- - Centralised env resolution (`env.ts`)
297
- - Tenant resolution separated into `tenant.ts` — single-tenant fallback
298
- - `mutateServerGraphQL` added (server-side mutations)
299
- - `ApiFactory` typed generics, batch partial-error reporting
300
- - `registerCookiesFn` removes hard `next/headers` dependency
301
- - `getServerCookieString` helper exported
302
- - `parseGraphQLErrors` exported for custom use
303
- - Subdomains[0] fallback for single-tenant setups
304
- - `uploadGraphQLMutation` no longer throws on missing tenant in single-tenant mode
1
+ # @econneq/gql-auth
2
+
3
+ GraphQL + Auth utilities for **multi-tenant** and **single-tenant** Next.js apps.
4
+
5
+ Handles:
6
+ - Server-side (internal Docker network) and client-side (browser proxy) GraphQL
7
+ - Multi-tenant header injection (`X-Tenant-ID`, `X-Process-ID`, `X-Widget-ID`)
8
+ - JWT auth on both server and client
9
+ - File uploads (GraphQL multipart spec)
10
+ - Batch mutations with per-item error reporting
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @econneq/gql-auth
18
+ ```
19
+
20
+ ### Peer dependencies
21
+
22
+ ```bash
23
+ npm install @apollo/client graphql next react
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Environment Variables
29
+
30
+ | Variable | Side | Description |
31
+ |---|---|---|
32
+ | `DOMAIN_URL` | server | Base domain, e.g. `e-conneq.com` |
33
+ | `PROTOCOL` | server | e.g. `https://` |
34
+ | `BACKEND_URL_INT` | server | Internal Docker URL, e.g. `api:8000` |
35
+ | `BACKEND_URL_EXT` | server | External backend host, e.g. `admin.e-conneq.com` |
36
+ | `NEXT_PUBLIC_DOMAIN_URL` | both | Same as DOMAIN_URL (client-accessible) |
37
+ | `NEXT_PUBLIC_PROTOCOL` | both | Same as PROTOCOL (client-accessible) |
38
+ | `NEXT_PUBLIC_BACKEND_URL_EXT` | both | Same as BACKEND_URL_EXT (client-accessible) |
39
+ | `API_KEY` | server | Optional service-to-service key |
40
+
41
+ > The internal URL (`BACKEND_URL_INT`) is **never** exposed to the browser.
42
+ > Client-side mutations route through Next.js `/api/graphql` proxy.
43
+
44
+ ---
45
+
46
+ ## Config Object
47
+
48
+ Every function accepts a `config` object describing your tenants.
49
+
50
+ ```ts
51
+ // config.ts (shared across your project)
52
+ import type { ConfigType } from "@econneq/gql-auth";
53
+
54
+ export const config: ConfigType = {
55
+ Subdomains: [
56
+ { id: 1, subdomain: "acme", processId: "proc-1", widgetID: "wgt-1" },
57
+ { id: 2, subdomain: "globex", processId: "proc-2" },
58
+ ],
59
+ };
60
+ ```
61
+
62
+ **Single-tenant**: put one entry in `Subdomains`. The library will always use it
63
+ without requiring a `domain` parameter.
64
+
65
+ ---
66
+
67
+ ## Quick start
68
+
69
+ ### 1. Register the cookie helper (Next.js app)
70
+
71
+ In your root layout (`app/layout.tsx`) or instrumentation file:
72
+
73
+ ```ts
74
+ import { cookies } from "next/headers";
75
+ import { registerCookiesFn } from "@econneq/gql-auth";
76
+
77
+ registerCookiesFn(cookies);
78
+ ```
79
+
80
+ This lets the Apollo client forward cookies server-side without a hard
81
+ `next/headers` import inside the package.
82
+
83
+ ---
84
+
85
+ ### 2. Server-side query (Server Component / Route Handler)
86
+
87
+ ```ts
88
+ import { queryServerGraphQL } from "@econneq/gql-auth";
89
+ import { config } from "@/config";
90
+ import { GET_USERS } from "@/graphql/queries";
91
+
92
+ // Multi-tenant
93
+ const data = await queryServerGraphQL({
94
+ query: GET_USERS,
95
+ variables: { active: true },
96
+ domain: "acme", // ← matches Subdomains[].subdomain
97
+ config,
98
+ });
99
+
100
+ // Single-tenant (omit domain)
101
+ const data = await queryServerGraphQL({
102
+ query: GET_USERS,
103
+ config,
104
+ });
105
+ ```
106
+
107
+ ### 3. Server-side mutation (Server Action)
108
+
109
+ ```ts
110
+ "use server";
111
+ import { mutateServerGraphQL } from "@econneq/gql-auth";
112
+
113
+ const result = await mutateServerGraphQL({
114
+ query: CREATE_USER_MUTATION,
115
+ variables: { input: formData },
116
+ domain: params.domain,
117
+ config,
118
+ });
119
+ ```
120
+
121
+ ---
122
+
123
+ ### 4. Client-side mutation with ApiFactory
124
+
125
+ ```tsx
126
+ "use client";
127
+ import { ApiFactory, getToken } from "@econneq/gql-auth";
128
+ import { useRouter } from "next/navigation";
129
+ import { config } from "@/config";
130
+
131
+ export function MyForm({ params }: { params: { domain: string } }) {
132
+ const router = useRouter();
133
+
134
+ const handleSubmit = async (formData: any) => {
135
+ await ApiFactory({
136
+ newData: formData,
137
+ mutationName: "createUser",
138
+ modelName: "user",
139
+ successField: "username",
140
+ query: CREATE_USER_MUTATION,
141
+ params: { domain: params.domain }, // omit for single-tenant
142
+ config,
143
+ router,
144
+ token: getToken() ?? undefined,
145
+ onAlert: ({ title, status }) => {
146
+ // Connect to your toast/alert system
147
+ console.log(title, status);
148
+ },
149
+ redirect: true,
150
+ redirectPath: "/dashboard",
151
+ });
152
+ };
153
+
154
+ return <form onSubmit={...}>...</form>;
155
+ }
156
+ ```
157
+
158
+ #### Batch mutations
159
+
160
+ Pass an array to `newData`:
161
+
162
+ ```ts
163
+ await ApiFactory({
164
+ newData: [item1, item2, item3],
165
+ mutationName: "createProduct",
166
+ modelName: "product",
167
+ // ...
168
+ });
169
+ ```
170
+
171
+ #### File uploads
172
+
173
+ ```ts
174
+ await ApiFactory({
175
+ newData: formData,
176
+ getFileMap: (item) => ({ avatar: item.avatarFile }),
177
+ // ...
178
+ });
179
+ ```
180
+
181
+ #### Return data instead of alert
182
+
183
+ ```ts
184
+ const user = await ApiFactory<{ id: string; username: string }>({
185
+ returnResponseObject: true,
186
+ // ...
187
+ });
188
+ ```
189
+
190
+ ---
191
+
192
+ ### 5. Direct upload mutation
193
+
194
+ For full control over multipart uploads:
195
+
196
+ ```ts
197
+ import { uploadGraphQLMutation } from "@econneq/gql-auth";
198
+
199
+ const response = await uploadGraphQLMutation({
200
+ query: UPLOAD_DOCUMENT_MUTATION.loc!.source.body,
201
+ variables: { title: "Report", file: null },
202
+ fileMap: { file: selectedFile },
203
+ params: { domain: "acme" },
204
+ config,
205
+ token: userToken,
206
+ });
207
+ ```
208
+
209
+ ---
210
+
211
+ ### 6. Auth utilities
212
+
213
+ #### Client-side
214
+
215
+ ```ts
216
+ import { getToken, getUser, logout } from "@econneq/gql-auth";
217
+
218
+ const token = getToken(); // cookie → localStorage
219
+ const user = getUser(); // decoded JwtPayload
220
+ logout(); // clears cookies + localStorage
221
+ ```
222
+
223
+ #### Server-side
224
+
225
+ ```ts
226
+ import {
227
+ getServerToken,
228
+ getServerUser,
229
+ getPermissions,
230
+ } from "@econneq/gql-auth";
231
+
232
+ const token = await getServerToken();
233
+ const user = await getServerUser();
234
+
235
+ // Does the user have any access to module 42?
236
+ const canRead = getPermissions(user?.permissions ?? [], 42);
237
+
238
+ // Does the user have CREATE access to module 42?
239
+ const canCreate = getPermissions(user?.permissions ?? [], 42, "C");
240
+ ```
241
+
242
+ ---
243
+
244
+ ### 7. Apollo client (advanced)
245
+
246
+ Direct access when you need a custom query/mutation outside ApiFactory:
247
+
248
+ ```ts
249
+ import { getApolloClient } from "@econneq/gql-auth";
250
+
251
+ const client = await getApolloClient({
252
+ domain: "acme", // omit for single-tenant
253
+ config,
254
+ });
255
+
256
+ const { data } = await client.query({ query: MY_QUERY });
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Architecture
262
+
263
+ ```
264
+ Browser Next.js Server Backend (Docker)
265
+ ───────── ────────────── ────────────────
266
+ uploadGraphQLMutation ──► /api/graphql (proxy) ──► api:8000/graphql/
267
+ ApiFactory ──► /api/graphql (proxy) ──► api:8000/graphql/
268
+
269
+ queryServerGraphQL ────────────────────────────► http://api:8000/graphql/
270
+ mutateServerGraphQL ────────────────────────────► http://api:8000/graphql/
271
+ getApolloClient ────────────────────────────► http://api:8000/graphql/
272
+ ```
273
+
274
+ Server-side calls skip the proxy entirely and hit the backend directly
275
+ through Docker's internal network — faster and no public exposure.
276
+
277
+ ---
278
+
279
+ ## Permissions format
280
+
281
+ Permission strings encode resource + action:
282
+
283
+ ```
284
+ "42" → access resource 42 (any action)
285
+ "42C" → create on resource 42
286
+ "42R" → read on resource 42
287
+ "42U" → update on resource 42
288
+ "42D" → delete on resource 42
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Changelog
294
+
295
+ ### 2.0.0
296
+ - Centralised env resolution (`env.ts`)
297
+ - Tenant resolution separated into `tenant.ts` — single-tenant fallback
298
+ - `mutateServerGraphQL` added (server-side mutations)
299
+ - `ApiFactory` typed generics, batch partial-error reporting
300
+ - `registerCookiesFn` removes hard `next/headers` dependency
301
+ - `getServerCookieString` helper exported
302
+ - `parseGraphQLErrors` exported for custom use
303
+ - Subdomains[0] fallback for single-tenant setups
304
+ - `uploadGraphQLMutation` no longer throws on missing tenant in single-tenant mode
@@ -23,7 +23,7 @@ export declare const getServerUser: () => Promise<JwtPayload | null>;
23
23
  * // Does the user have CREATE access to module 42?
24
24
  * getPermissions(user.permissions, 42, "C");
25
25
  */
26
- export declare const getPermissions: (list: string[], access: number, action?: string) => boolean;
26
+ export declare const getPermissions: (list: string[], access: number, action?: string) => Promise<boolean>;
27
27
  /**
28
28
  * Return the full cookie header string for forwarding to the
29
29
  * backend (Django session / CSRF pass-through).
@@ -1 +1 @@
1
- {"version":3,"file":"authServer.d.ts","sourceRoot":"","sources":["../src/authServer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAI1C;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAa,OAAO,CAAC,MAAM,GAAG,IAAI,CAS5D,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAa,OAAO,CAAC,UAAU,GAAG,IAAI,CAU/D,CAAC;AAIF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EAAE,EACd,QAAQ,MAAM,EACd,SAAS,MAAM,KACd,OAoBF,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,MAAM,CAQ5D,CAAC"}
1
+ {"version":3,"file":"authServer.d.ts","sourceRoot":"","sources":["../src/authServer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAI1C;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAa,OAAO,CAAC,MAAM,GAAG,IAAI,CAS5D,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAa,OAAO,CAAC,UAAU,GAAG,IAAI,CAU/D,CAAC;AAIF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EAAE,EACd,QAAQ,MAAM,EACd,SAAS,MAAM,KACd,OAAO,CAAC,OAAO,CAoBjB,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,MAAM,CAQ5D,CAAC"}
@@ -55,7 +55,7 @@ export const getServerUser = async () => {
55
55
  * // Does the user have CREATE access to module 42?
56
56
  * getPermissions(user.permissions, 42, "C");
57
57
  */
58
- export const getPermissions = (list, access, action) => {
58
+ export const getPermissions = async (list, access, action) => {
59
59
  if (!list?.length)
60
60
  return false;
61
61
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"authServer.js","sourceRoot":"","sources":["../src/authServer.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+CAA+C;AAC/C,+DAA+D;AAC/D,EAAE;AACF,8DAA8D;AAC9D,4DAA4D;AAC5D,kBAAkB;AAClB,gEAAgE;AAEhE,YAAY,CAAC;AAIb,gEAAgE;AAEhE;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,IAA4B,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,iEAAiE;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,IAAgC,EAAE;IAClE,MAAM,KAAK,GAAG,MAAM,cAAc,EAAE,CAAC;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,CAAC;QACH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;QACjD,OAAO,SAAS,CAAa,KAAK,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,gEAAgE;AAEhE;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,IAAc,EACd,MAAc,EACd,MAAe,EACN,EAAE;IACX,IAAI,CAAC,IAAI,EAAE,MAAM;QAAE,OAAO,KAAK,CAAC;IAEhC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACzB,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;gBACtB,iDAAiD;gBACjD,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,MAAM,SAAS,CAAC,CAAC;gBAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3B,CAAC;YAED,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;gBACrB,OAAO,KAAK,KAAK,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC;YACxC,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF,iEAAiE;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,IAAqB,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,OAAO,WAAW,CAAC,QAAQ,EAAE,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC"}
1
+ {"version":3,"file":"authServer.js","sourceRoot":"","sources":["../src/authServer.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+CAA+C;AAC/C,+DAA+D;AAC/D,EAAE;AACF,8DAA8D;AAC9D,4DAA4D;AAC5D,kBAAkB;AAClB,gEAAgE;AAEhE,YAAY,CAAC;AAIb,gEAAgE;AAEhE;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,IAA4B,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,iEAAiE;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,IAAgC,EAAE;IAClE,MAAM,KAAK,GAAG,MAAM,cAAc,EAAE,CAAC;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,CAAC;QACH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;QACjD,OAAO,SAAS,CAAa,KAAK,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,gEAAgE;AAEhE;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,IAAc,EACd,MAAc,EACd,MAAe,EACG,EAAE;IACpB,IAAI,CAAC,IAAI,EAAE,MAAM;QAAE,OAAO,KAAK,CAAC;IAEhC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACzB,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;gBACtB,iDAAiD;gBACjD,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,MAAM,SAAS,CAAC,CAAC;gBAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3B,CAAC;YAED,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;gBACrB,OAAO,KAAK,KAAK,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC;YACxC,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF,iEAAiE;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,IAAqB,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,OAAO,WAAW,CAAC,QAAQ,EAAE,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { InterFieldList, JwtPayload } from ".";
1
+ import { InterFieldList, JwtPayload } from "./index.js";
2
2
  export declare const capitalizeEachWord: (str: string) => string;
3
3
  export declare const errorLog: (err: any, show?: boolean) => string;
4
4
  export declare const logout: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"utilsGeneral.d.ts","sourceRoot":"","sources":["../src/utilsGeneral.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC;AAM/C,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,WAM7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,KAAK,GAAG,EAAE,OAAO,OAAO,WAyBhD,CAAC;AAEF,eAAO,MAAM,MAAM,YAclB,CAAC;AAEF,eAAO,MAAM,QAAQ,qBAKpB,CAAC;AAEF,eAAO,MAAM,OAAO,yBAKnB,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,EAAE,WAO5C,CAAC;AAEF,eAAO,MAAM,UAAU,GACnB,SAAS,MAAM,EACf,MAAM,MAAM,GAAG,QAAQ,KACxB,MA0CF,CAAC;AAEF,eAAO,MAAM,WAAW,mBAOvB,CAAC;AAEF,eAAO,MAAM,WAAW,cAMvB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,WAIxC,CAAA;AAED,eAAO,MAAM,iBAAiB,GAAI,KAAK,GAAG,QAUzC,CAAC;AAEF,eAAO,MAAM,KAAK,GACd,6BACI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,SAU7F,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,UAAU,GAAG,EAAE,mBAAmB,cAAc,EAAE,2BAkDpF,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;GAKvC,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,WAiDtC,CAAC"}
1
+ {"version":3,"file":"utilsGeneral.d.ts","sourceRoot":"","sources":["../src/utilsGeneral.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAMxD,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,WAM7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,KAAK,GAAG,EAAE,OAAO,OAAO,WAyBhD,CAAC;AAEF,eAAO,MAAM,MAAM,YAclB,CAAC;AAEF,eAAO,MAAM,QAAQ,qBAKpB,CAAC;AAEF,eAAO,MAAM,OAAO,yBAKnB,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,EAAE,WAO5C,CAAC;AAEF,eAAO,MAAM,UAAU,GACnB,SAAS,MAAM,EACf,MAAM,MAAM,GAAG,QAAQ,KACxB,MA0CF,CAAC;AAEF,eAAO,MAAM,WAAW,mBAOvB,CAAC;AAEF,eAAO,MAAM,WAAW,cAMvB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,WAIxC,CAAA;AAED,eAAO,MAAM,iBAAiB,GAAI,KAAK,GAAG,QAUzC,CAAC;AAEF,eAAO,MAAM,KAAK,GACd,6BACI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,SAU7F,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,UAAU,GAAG,EAAE,mBAAmB,cAAc,EAAE,2BAkDpF,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;GAKvC,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,WAiDtC,CAAC"}
package/package.json CHANGED
@@ -1,17 +1,11 @@
1
1
  {
2
2
  "name": "@econneq/gql-auth",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "GraphQL auth utilities for multi-tenant and single-tenant Next.js projects.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
- "scripts": {
10
- "build": "tsc",
11
- "build:watch": "tsc --watch",
12
- "prepublishOnly": "npm run build",
13
- "typecheck": "tsc --noEmit"
14
- },
15
9
  "publishConfig": {
16
10
  "access": "public"
17
11
  },
@@ -72,5 +66,10 @@
72
66
  "@types/node": "^20.0.0",
73
67
  "@types/react": "^19.0.0",
74
68
  "typescript": "^5.0.0"
69
+ },
70
+ "scripts": {
71
+ "build": "tsc",
72
+ "build:watch": "tsc --watch",
73
+ "typecheck": "tsc --noEmit"
75
74
  }
76
- }
75
+ }
package/dist/utils.d.ts DELETED
@@ -1,24 +0,0 @@
1
- import { InterFieldList, JwtPayload } from ".";
2
- export declare const capitalizeEachWord: (str: string) => string;
3
- export declare const errorLog: (err: any, show?: boolean) => string;
4
- export declare const getToken: () => string | null;
5
- export declare const getUser: () => JwtPayload | null;
6
- export declare const getStoredFont: (FONTS: string[]) => string;
7
- export declare const getTimeAgo: (dateStr: string, type: "past" | "future") => string;
8
- export declare const getLanguage: () => "en" | "fr";
9
- export declare const getTemplate: () => number;
10
- export declare const decodeUrlID: (urlID: string) => string;
11
- export declare function getAcademicYear(): string;
12
- export declare const removeEmptyFields: (obj: any) => any;
13
- export declare const Alert: ({ title, duration, status }: {
14
- title: string;
15
- duration: 1000 | 1500 | 2000 | 3000 | 4000 | 5000;
16
- status?: boolean;
17
- }) => void;
18
- export declare const validateFormFields: (formData: any, currentStepFields: InterFieldList[]) => Record<string, string>;
19
- export declare const lastYears: (number: number) => {
20
- value: string;
21
- label: string;
22
- }[];
23
- export declare const formatText: (text: string) => string;
24
- //# sourceMappingURL=utils.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC;AAI/C,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,WAM7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,KAAK,GAAG,EAAE,OAAO,OAAO,WAyBhD,CAAC;AAKF,eAAO,MAAM,QAAQ,qBAKpB,CAAC;AAEF,eAAO,MAAM,OAAO,yBAKnB,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,EAAE,WAO5C,CAAC;AAEF,eAAO,MAAM,UAAU,GACnB,SAAS,MAAM,EACf,MAAM,MAAM,GAAG,QAAQ,KACxB,MA0CF,CAAC;AAGF,eAAO,MAAM,WAAW,mBAOvB,CAAC;AAEF,eAAO,MAAM,WAAW,cAMvB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,WAIxC,CAAA;AAED,wBAAgB,eAAe,IAAI,MAAM,CAUxC;AAED,eAAO,MAAM,iBAAiB,GAAI,KAAK,GAAG,QAUzC,CAAC;AAGF,eAAO,MAAM,KAAK,GACd,6BACI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,SAU7F,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,UAAU,GAAG,EAAE,mBAAmB,cAAc,EAAE,2BAkDpF,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;GAKvC,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,WAiDtC,CAAC"}
package/dist/utils.js DELETED
@@ -1,257 +0,0 @@
1
- import Swal from "sweetalert2";
2
- import Cookies from "js-cookie";
3
- import { jwtDecode } from "jwt-decode";
4
- export const capitalizeEachWord = (str) => {
5
- if (!str)
6
- return ''; // Handle empty or null strings
7
- return str
8
- .split(' ')
9
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
10
- .join(' ');
11
- };
12
- export const errorLog = (err, show) => {
13
- let mes = "An unknown error occurred";
14
- if (typeof err === "string")
15
- mes = err;
16
- else if (err?.graphQLErrors?.length > 0)
17
- mes = err.graphQLErrors.map((e) => e.message).join('\n');
18
- else if (err?.networkError) {
19
- const netErr = err.networkError;
20
- if ("result" in netErr && netErr.result?.errors?.length > 0)
21
- mes = netErr.result.errors.map((e) => e.message).join('\n');
22
- else if (netErr.message)
23
- mes = netErr.message;
24
- }
25
- else if (err?.extraInfo)
26
- mes = String(err.extraInfo);
27
- else if (err?.message)
28
- mes = err.message;
29
- if (show) {
30
- Swal.fire({
31
- title: mes,
32
- icon: 'error',
33
- timer: 3000,
34
- timerProgressBar: true,
35
- showConfirmButton: false,
36
- });
37
- }
38
- return mes;
39
- };
40
- const currentYear = new Date().getFullYear();
41
- export const getToken = () => {
42
- if (typeof window === "undefined") {
43
- return null;
44
- }
45
- return Cookies.get("token") || localStorage.getItem("token");
46
- };
47
- export const getUser = () => {
48
- // const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
49
- const token = getToken();
50
- const user = token ? jwtDecode(token) : null;
51
- return user;
52
- };
53
- export const getStoredFont = (FONTS) => {
54
- if (typeof window === "undefined")
55
- return FONTS[0]; // Default for SSR
56
- const up = localStorage.getItem("user-pref");
57
- if (!up)
58
- return FONTS[5]; // Default to Ubuntu (6th index)
59
- const fontIndex = parseInt(up[1]) - 1;
60
- return FONTS[fontIndex] || FONTS[5];
61
- };
62
- export const getTimeAgo = (dateStr, type) => {
63
- if (!dateStr)
64
- return "";
65
- const now = new Date();
66
- const item = new Date(dateStr);
67
- let diffMs = 0;
68
- if (type === "past")
69
- diffMs = now.getTime() - item.getTime();
70
- if (type === "future")
71
- diffMs = item.getTime() - now.getTime();
72
- // Handle invalid date ranges
73
- if (diffMs < 0) {
74
- return type === "past" ? "just now" : "Closed";
75
- }
76
- const seconds = Math.floor(diffMs / 1000);
77
- const minutes = Math.floor(seconds / 60);
78
- const hours = Math.floor(minutes / 60);
79
- const days = Math.floor(hours / 24);
80
- const weeks = Math.floor(days / 7);
81
- const months = Math.floor(days / 30.44); // Use average month length
82
- const years = Math.floor(days / 365.25); // Account for leap years
83
- const suffix = type === "past" ? "ago" : "left";
84
- // 1. Logic for Past Dates (e.g., Posted 2 days ago)
85
- if (type === "past") {
86
- if (days === 0)
87
- return "Today";
88
- if (days === 1)
89
- return "Yesterday";
90
- if (days < 7)
91
- return `${days} days ago`;
92
- if (weeks < 5)
93
- return `${weeks} week${weeks > 1 ? "s" : ""} ago`;
94
- if (months < 12)
95
- return `${months} month${months > 1 ? "s" : ""} ago`;
96
- return `${years} year${years > 1 ? "s" : ""} ago`;
97
- }
98
- // 2. Logic for Future Dates (e.g., 5 days left)
99
- if (days === 0)
100
- return "Ends today";
101
- if (days === 1)
102
- return "1 day left";
103
- if (days < 7)
104
- return `${days} days left`;
105
- if (weeks < 5)
106
- return `${weeks} week${weeks > 1 ? "s" : ""} left`;
107
- if (months < 12)
108
- return `${months} month${months > 1 ? "s" : ""} left`;
109
- return `${years} year${years > 1 ? "s" : ""} left`;
110
- };
111
- export const getLanguage = () => {
112
- if (typeof window === "undefined")
113
- return "en";
114
- const up = localStorage.getItem("user-pref");
115
- if (!up)
116
- return "en";
117
- const langNumber = parseInt(up[0]);
118
- return langNumber === 1 ? "en" : "fr";
119
- };
120
- export const getTemplate = () => {
121
- if (typeof window === "undefined")
122
- return 1;
123
- const up = localStorage.getItem("user-pref");
124
- if (!up)
125
- return 1;
126
- return parseInt(up[2]);
127
- };
128
- export const decodeUrlID = (urlID) => {
129
- const base64DecodedString = decodeURIComponent(urlID); // Decodes %3D%3D to ==
130
- const id = Buffer.from(base64DecodedString, 'base64').toString('utf-8'); // Decoding from base64
131
- return id.split(":")[1];
132
- };
133
- export function getAcademicYear() {
134
- const today = new Date();
135
- const year = today.getFullYear();
136
- const month = today.getMonth(); // 0 = January, 7 = August
137
- if (month < 7) {
138
- return `${year - 1}/${year}`;
139
- }
140
- else {
141
- return `${year}/${year + 1}`;
142
- }
143
- }
144
- export const removeEmptyFields = (obj) => {
145
- const newObj = {};
146
- for (const key in obj) {
147
- // Keep File objects and non-empty values
148
- if (obj[key] instanceof File ||
149
- (obj[key] !== null && obj[key] !== undefined && obj[key] !== '')) {
150
- newObj[key] = obj[key];
151
- }
152
- }
153
- return newObj;
154
- };
155
- export const Alert = ({ title, duration, status = true }) => {
156
- Swal.fire({
157
- title: capitalizeEachWord(title),
158
- timer: duration,
159
- timerProgressBar: true,
160
- showConfirmButton: false,
161
- icon: status ? 'success' : 'error',
162
- });
163
- };
164
- export const validateFormFields = (formData, currentStepFields) => {
165
- const newErrors = {};
166
- // 1. Flatten the fields from all rows in the step
167
- const fieldsToValidate = currentStepFields.flatMap(row => row.fields);
168
- fieldsToValidate.forEach((field) => {
169
- // Skip validation if the field is hidden
170
- if (field.show === false)
171
- return;
172
- // 2. Get the value from formData
173
- const value = formData[field.name];
174
- // 3. Check Required
175
- if (field.required && (!value || value.toString().trim() === "")) {
176
- newErrors[field.name] = `${capitalizeEachWord(field.label)} is required`;
177
- // return;
178
- }
179
- // 4. Check Type (Email)
180
- if (field.type === 'email' && value) {
181
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
182
- if (!emailRegex.test(value)) {
183
- newErrors[field.name] = "Invalid email format";
184
- }
185
- }
186
- // 5. Check Numbers (Min/Max)
187
- if (field.type === 'number' && value) {
188
- const numValue = Number(value);
189
- if (field.min !== undefined && numValue < field.min) {
190
- newErrors[field.name] = `Minimum value is ${field.min}`;
191
- }
192
- if (field.max !== undefined && numValue > field.max) {
193
- newErrors[field.name] = `Maximum value is ${field.max}`;
194
- }
195
- }
196
- // 6. Check Length (Strings)
197
- if (field.type === 'text' && value) {
198
- if (field.min !== undefined && value.length < field.min) {
199
- newErrors[field.name] = `Too short (min ${field.min} chars)`;
200
- }
201
- if (field.max !== undefined && value.length > field.max) {
202
- newErrors[field.name] = `Too long (min ${field.max} chars)`;
203
- }
204
- }
205
- });
206
- return newErrors;
207
- };
208
- export const lastYears = (number) => {
209
- return Array.from({ length: number }, (_, i) => ({
210
- value: (currentYear - i).toString(),
211
- label: (currentYear - i).toString()
212
- })).sort((a, b) => Number(b.value) - Number(a.value));
213
- };
214
- export const formatText = (text) => {
215
- return text;
216
- if (!text || typeof text !== "string")
217
- return "";
218
- return text
219
- // Ensure space after "." if it's not part of abbreviation/number
220
- .replace(/\.(?!\s|$|[a-zA-Z0-9])/g, ". ")
221
- // Split sentences by ". " but preserve delimiter
222
- .split(/(\. )/g)
223
- .map((segment) => {
224
- if (segment === ". ")
225
- return segment; // keep delimiter
226
- return segment
227
- .split(" ")
228
- .map((word, i) => {
229
- if (word === "")
230
- return word;
231
- // Preserve existing ALL CAPS words
232
- if (word.length > 1 && /^[A-Z]+$/.test(word)) {
233
- return word;
234
- }
235
- // Abbreviations like g.a, n.l.n
236
- if (/^([a-zA-Z]\.)+[a-zA-Z]?$/.test(word)) {
237
- return word.toUpperCase(); // → G.A, N.L.N
238
- }
239
- // Version numbers like 3.0.1
240
- if (/^\d+(\.\d+)+$/.test(word)) {
241
- return word;
242
- }
243
- // Mixed abbrev/numbers (like p.m2.5 or covid-19 v2.0)
244
- if (/[a-zA-Z]\.\d/.test(word) || /\d+\.\d+[a-zA-Z]?/.test(word)) {
245
- return word; // leave as-is
246
- }
247
- // First word of sentence → capitalize first letter
248
- if (i === 0) {
249
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
250
- }
251
- // Other words → lowercase
252
- return word.toLowerCase();
253
- })
254
- .join(" ");
255
- })
256
- .join("");
257
- };