zone4code-sdk 1.0.2 → 1.0.3

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Official TypeScript & JavaScript Client SDK for the **Zone4Code Platform** & **Singulary**.
4
4
 
5
- Provides a type-safe, chainable interface (similar to Supabase / Prisma) for **Keycloak Authentication**, **Adaptive Object-Model (AOM) Dynamic Schemas**, and **JSONB Querying** over the Unified API Gateway.
5
+ Provides a type-safe, chainable interface (similar to Supabase / Prisma) for **Keycloak Authentication**, **Adaptive Object-Model (AOM) Dynamic Schemas**, **JSONB Querying**, **Audit Revisions**, **Graph Relations**, and **Workflow Plugins** over the Unified API Gateway.
6
6
 
7
7
  ---
8
8
 
@@ -22,11 +22,12 @@ pnpm add zone4code-sdk
22
22
  import { createClient } from 'zone4code-sdk';
23
23
 
24
24
  const client = createClient({
25
- gatewayUrl: 'http://localhost:8080', // Or your remote server: https://api.yourdomain.com
26
- tenantId: 'my-workspace-id'
25
+ gatewayUrl: 'http://localhost:8080', // Or your production server: https://api.yourdomain.com
26
+ tenantId: 'my-workspace-id',
27
+ // storageKey: 'stayz.tokens' // Optional custom storage key name
27
28
  });
28
29
 
29
- // 1. Check Gateway Health
30
+ // Check Gateway Health
30
31
  const health = await client.health();
31
32
  console.log('Connected to Gateway:', health.licenseTier);
32
33
  ```
@@ -35,25 +36,60 @@ console.log('Connected to Gateway:', health.licenseTier);
35
36
 
36
37
  ## 🔐 Authentication
37
38
 
39
+ ### 1. Registration & Auto-Login
40
+ Registers a user in Keycloak (`POST /auth/:tenantId/user`) and automatically signs them in:
38
41
  ```typescript
39
- // Register a new user
40
- await client.auth.register({
42
+ const { token, user } = await client.auth.register({
41
43
  email: 'alice@example.com',
42
44
  password: 'SecurePassword123!',
43
45
  name: 'Alice Smith'
44
46
  });
47
+ ```
45
48
 
46
- // Login (JWT token is automatically saved to localStorage/Memory)
47
- const { token } = await client.auth.login({
49
+ ### 2. Login
50
+ Accepts `email`, `username`, or `login`:
51
+ ```typescript
52
+ const { token, refreshToken, user } = await client.auth.login({
48
53
  email: 'alice@example.com',
49
54
  password: 'SecurePassword123!'
50
55
  });
56
+ ```
57
+
58
+ ### 3. Synchronous User Inspection (`getUser`)
59
+ Decodes the active JWT token instantaneously without any network overhead:
60
+ ```typescript
61
+ const user = client.auth.getUser();
62
+ console.log(user?.id, user?.email, user?.roles);
63
+ ```
64
+
65
+ ### 4. Password Recovery & Reset
66
+ ```typescript
67
+ // Send recovery email
68
+ await client.auth.forgotPassword('alice@example.com');
69
+
70
+ // Complete reset with action token
71
+ await client.auth.resetPassword({
72
+ token: actionTokenFromEmail,
73
+ newPassword: 'NewSecurePassword123!'
74
+ });
51
75
 
52
- // Check status & profile
53
- if (client.auth.isAuthenticated()) {
54
- const profile = await client.auth.getProfile();
55
- console.log('Logged in as:', profile.email);
56
- }
76
+ // Change password (while logged in)
77
+ await client.auth.changePassword({
78
+ oldPassword: 'OldPassword123!',
79
+ newPassword: 'NewSecurePassword123!'
80
+ });
81
+ ```
82
+
83
+ ### 5. Token Renewal & Google OAuth
84
+ ```typescript
85
+ // Silent token renewal
86
+ await client.auth.refreshToken();
87
+
88
+ // Google OAuth login
89
+ await client.auth.loginWithGoogle(googleIdToken);
90
+
91
+ // Update user profile attributes
92
+ await client.auth.updateProfile({ firstName: 'Alice', attributes: { theme: 'dark' } });
57
93
 
58
94
  // Logout
59
95
  client.auth.logout();
@@ -90,14 +126,56 @@ console.log(`Found ${total} orders:`, data);
90
126
  ### 3. Update & Delete
91
127
  ```typescript
92
128
  // Update
93
- await client.from('order').update(orderId, {
94
- status: 'paid'
95
- });
129
+ await client.from('order').update(orderId, { status: 'paid' });
96
130
 
97
131
  // Soft Delete
98
132
  await client.from('order').delete(orderId);
99
133
  ```
100
134
 
135
+ ### 4. Revisions & Audit History
136
+ ```typescript
137
+ const revisions = await client.from('order').revisions(orderId);
138
+ console.log('Audit history:', revisions);
139
+ ```
140
+
141
+ ### 5. Graph Relations (Linking Records)
142
+ ```typescript
143
+ // Link order to a client record
144
+ await client.from('order').link(orderId, {
145
+ targetTypeName: 'client',
146
+ targetId: clientId,
147
+ relationName: 'belongs_to'
148
+ });
149
+
150
+ // Fetch all relations
151
+ const relations = await client.from('order').getRelations(orderId);
152
+
153
+ // Unlink
154
+ await client.from('order').unlink(orderId, 'belongs_to', clientId, 'client');
155
+ ```
156
+
157
+ ### 6. Workflow Plugin Actions
158
+ ```typescript
159
+ // Type-level action (e.g. checkout quote)
160
+ await client.from('quote').action('checkout', { quoteId });
161
+
162
+ // Instance-level action (e.g. cancel order)
163
+ await client.from('order').instanceAction(orderId, 'cancel', { reason: 'Customer request' });
164
+ ```
165
+
166
+ ### 7. Export CSV
167
+ ```typescript
168
+ // Returns raw CSV string matching active filters
169
+ const csv = await client.from('order').eq('status', 'paid').exportCsv();
170
+ ```
171
+
172
+ ### 8. User Profile & Wallet (`/me`)
173
+ ```typescript
174
+ // Returns user profile + loyalty wallet points from generic-api
175
+ const me = await client.getMe();
176
+ console.log(me.name, me.wallet?.available_points);
177
+ ```
178
+
101
179
  ---
102
180
 
103
181
  ## 🛠️ Dynamic Schema Alteration
package/dist/index.cjs CHANGED
@@ -75,7 +75,9 @@ var AuthClient = class _AuthClient {
75
75
  tenantId;
76
76
  storage;
77
77
  storageKey;
78
+ refreshStorageKey;
78
79
  token = null;
80
+ refreshTokenValue = null;
79
81
  fetchFn;
80
82
  static TOKEN_KEY_PREFIX = "z4c_token_";
81
83
  constructor(options) {
@@ -83,6 +85,7 @@ var AuthClient = class _AuthClient {
83
85
  this.tenantId = options.tenantId;
84
86
  this.storage = options.storage;
85
87
  this.storageKey = options.storageKey || _AuthClient.TOKEN_KEY_PREFIX + this.tenantId;
88
+ this.refreshStorageKey = this.storageKey + "_refresh";
86
89
  this.fetchFn = options.fetchFn || (typeof fetch !== "undefined" ? fetch : globalThis.fetch);
87
90
  if (options.initialToken) {
88
91
  this.setToken(options.initialToken);
@@ -91,11 +94,18 @@ var AuthClient = class _AuthClient {
91
94
  if (stored) this.token = stored;
92
95
  }).catch(() => {
93
96
  });
97
+ Promise.resolve(this.storage.getItem(this.refreshStorageKey)).then((stored) => {
98
+ if (stored) this.refreshTokenValue = stored;
99
+ }).catch(() => {
100
+ });
94
101
  }
95
102
  }
96
103
  getToken() {
97
104
  return this.token;
98
105
  }
106
+ getRefreshToken() {
107
+ return this.refreshTokenValue;
108
+ }
99
109
  setToken(token) {
100
110
  this.token = token;
101
111
  if (token) {
@@ -104,6 +114,14 @@ var AuthClient = class _AuthClient {
104
114
  this.storage.removeItem(this.storageKey);
105
115
  }
106
116
  }
117
+ setRefreshToken(token) {
118
+ this.refreshTokenValue = token;
119
+ if (token) {
120
+ this.storage.setItem(this.refreshStorageKey, token);
121
+ } else {
122
+ this.storage.removeItem(this.refreshStorageKey);
123
+ }
124
+ }
107
125
  isAuthenticated() {
108
126
  return !!this.token;
109
127
  }
@@ -212,6 +230,9 @@ var AuthClient = class _AuthClient {
212
230
  if (token) {
213
231
  this.setToken(token);
214
232
  }
233
+ if (json.refresh_token) {
234
+ this.setRefreshToken(json.refresh_token);
235
+ }
215
236
  const user = this.getUser() || json.user;
216
237
  return {
217
238
  token: token || "",
@@ -220,6 +241,163 @@ var AuthClient = class _AuthClient {
220
241
  ...json
221
242
  };
222
243
  }
244
+ /**
245
+ * Authenticate via Google OAuth ID token
246
+ */
247
+ async loginWithGoogle(token) {
248
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/login/google`;
249
+ const res = await this.fetchFn(url, {
250
+ method: "POST",
251
+ headers: {
252
+ "Content-Type": "application/json",
253
+ "x-tenant-id": this.tenantId
254
+ },
255
+ body: JSON.stringify({ token })
256
+ });
257
+ if (!res.ok) {
258
+ const err = await res.json().catch(() => ({ message: res.statusText }));
259
+ throw new Error(err.message || err.error || `Google login failed with HTTP ${res.status}`);
260
+ }
261
+ const json = await res.json();
262
+ const jwt = json.token || json.access_token;
263
+ if (jwt) {
264
+ this.setToken(jwt);
265
+ }
266
+ if (json.refresh_token) {
267
+ this.setRefreshToken(json.refresh_token);
268
+ }
269
+ const user = this.getUser() || json.user;
270
+ return {
271
+ token: jwt || "",
272
+ refreshToken: json.refresh_token,
273
+ user,
274
+ ...json
275
+ };
276
+ }
277
+ /**
278
+ * Renew expired access token using stored or provided refresh token
279
+ */
280
+ async refreshToken(refreshToken) {
281
+ const tokenToUse = refreshToken || this.getRefreshToken();
282
+ if (!tokenToUse) {
283
+ throw new Error("No refresh token available");
284
+ }
285
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/refresh`;
286
+ const res = await this.fetchFn(url, {
287
+ method: "POST",
288
+ headers: {
289
+ "Content-Type": "application/json",
290
+ "x-tenant-id": this.tenantId
291
+ },
292
+ body: JSON.stringify({ refresh_token: tokenToUse })
293
+ });
294
+ if (!res.ok) {
295
+ const err = await res.json().catch(() => ({ message: res.statusText }));
296
+ throw new Error(err.message || err.error || `Token refresh failed with HTTP ${res.status}`);
297
+ }
298
+ const json = await res.json();
299
+ const token = json.token || json.access_token;
300
+ if (token) {
301
+ this.setToken(token);
302
+ }
303
+ if (json.refresh_token) {
304
+ this.setRefreshToken(json.refresh_token);
305
+ }
306
+ const user = this.getUser() || json.user;
307
+ return {
308
+ token: token || "",
309
+ refreshToken: json.refresh_token,
310
+ user,
311
+ ...json
312
+ };
313
+ }
314
+ /**
315
+ * Request password reset email
316
+ */
317
+ async forgotPassword(data) {
318
+ const payload = typeof data === "string" ? { email: data } : data;
319
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/auth/forgot-password`;
320
+ const res = await this.fetchFn(url, {
321
+ method: "POST",
322
+ headers: {
323
+ "Content-Type": "application/json",
324
+ "x-tenant-id": this.tenantId
325
+ },
326
+ body: JSON.stringify(payload)
327
+ });
328
+ const json = await res.json().catch(() => ({}));
329
+ if (!res.ok) {
330
+ throw new Error(json.message || json.error || `Forgot password request failed with HTTP ${res.status}`);
331
+ }
332
+ return json;
333
+ }
334
+ /**
335
+ * Complete password reset using action token
336
+ */
337
+ async resetPassword(data) {
338
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/auth/reset-password`;
339
+ const res = await this.fetchFn(url, {
340
+ method: "POST",
341
+ headers: {
342
+ "Content-Type": "application/json",
343
+ "x-tenant-id": this.tenantId
344
+ },
345
+ body: JSON.stringify(data)
346
+ });
347
+ const json = await res.json().catch(() => ({}));
348
+ if (!res.ok) {
349
+ throw new Error(json.message || json.error || `Reset password failed with HTTP ${res.status}`);
350
+ }
351
+ return json;
352
+ }
353
+ /**
354
+ * Change password for current or specified user
355
+ */
356
+ async changePassword(data, userId) {
357
+ const targetUserId = userId || this.getUser()?.id;
358
+ if (!targetUserId) {
359
+ throw new Error("User ID required: user is not logged in and no userId was provided");
360
+ }
361
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/user/${targetUserId}/change-password`;
362
+ const res = await this.fetchFn(url, {
363
+ method: "POST",
364
+ headers: {
365
+ "Content-Type": "application/json",
366
+ "x-tenant-id": this.tenantId,
367
+ ...this.token ? { "Authorization": `Bearer ${this.token}` } : {}
368
+ },
369
+ body: JSON.stringify(data)
370
+ });
371
+ if (!res.ok) {
372
+ const err = await res.json().catch(() => ({ message: res.statusText }));
373
+ throw new Error(err.message || err.error || `Change password failed with HTTP ${res.status}`);
374
+ }
375
+ return res.json().catch(() => ({ success: true }));
376
+ }
377
+ /**
378
+ * Update profile attributes for current or specified user
379
+ */
380
+ async updateProfile(data, userId) {
381
+ const targetUserId = userId || this.getUser()?.id;
382
+ if (!targetUserId) {
383
+ throw new Error("User ID required: user is not logged in and no userId was provided");
384
+ }
385
+ const url = `${this.gatewayUrl}/auth/${this.tenantId}/user/${targetUserId}`;
386
+ const res = await this.fetchFn(url, {
387
+ method: "PUT",
388
+ headers: {
389
+ "Content-Type": "application/json",
390
+ "x-tenant-id": this.tenantId,
391
+ ...this.token ? { "Authorization": `Bearer ${this.token}` } : {}
392
+ },
393
+ body: JSON.stringify(data)
394
+ });
395
+ if (!res.ok) {
396
+ const err = await res.json().catch(() => ({ message: res.statusText }));
397
+ throw new Error(err.message || err.error || `Update profile failed with HTTP ${res.status}`);
398
+ }
399
+ return res.json();
400
+ }
223
401
  /**
224
402
  * Fetch current authenticated user profile
225
403
  */
@@ -245,10 +423,11 @@ var AuthClient = class _AuthClient {
245
423
  return res.json();
246
424
  }
247
425
  /**
248
- * Log out and clear saved token
426
+ * Log out and clear saved tokens
249
427
  */
250
428
  logout() {
251
429
  this.setToken(null);
430
+ this.setRefreshToken(null);
252
431
  }
253
432
  };
254
433
 
@@ -484,6 +663,132 @@ var EntityQueryBuilder = class {
484
663
  }
485
664
  return { success: true };
486
665
  }
666
+ /**
667
+ * Fetch revision history for an entity record
668
+ */
669
+ async revisions(id) {
670
+ const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/revisions`;
671
+ const res = await this.fetchFn(url, {
672
+ headers: this.getHeaders()
673
+ });
674
+ if (!res.ok) {
675
+ const err = await res.json().catch(() => ({ message: res.statusText }));
676
+ throw new Error(err.message || `Failed to fetch revisions for ${this.typeName}/${id}`);
677
+ }
678
+ return res.json();
679
+ }
680
+ /**
681
+ * Fetch all graph relations for this entity record
682
+ */
683
+ async getRelations(id, lang) {
684
+ const qs = lang ? `?lang=${encodeURIComponent(lang)}` : "";
685
+ const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations${qs}`;
686
+ const res = await this.fetchFn(url, {
687
+ headers: this.getHeaders()
688
+ });
689
+ if (!res.ok) {
690
+ const err = await res.json().catch(() => ({ message: res.statusText }));
691
+ throw new Error(err.message || `Failed to fetch relations for ${this.typeName}/${id}`);
692
+ }
693
+ return res.json();
694
+ }
695
+ /**
696
+ * Link this record to another entity record
697
+ */
698
+ async link(id, payload) {
699
+ const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
700
+ const res = await this.fetchFn(url, {
701
+ method: "POST",
702
+ headers: this.getHeaders(),
703
+ body: JSON.stringify(payload)
704
+ });
705
+ if (!res.ok) {
706
+ const err = await res.json().catch(() => ({ message: res.statusText }));
707
+ throw new Error(err.message || `Failed to create relation for ${this.typeName}/${id}`);
708
+ }
709
+ return res.json().catch(() => ({ success: true }));
710
+ }
711
+ /**
712
+ * Replace the full set of relations of one relation name
713
+ */
714
+ async setLinks(id, payload) {
715
+ const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations`;
716
+ const res = await this.fetchFn(url, {
717
+ method: "PUT",
718
+ headers: this.getHeaders(),
719
+ body: JSON.stringify(payload)
720
+ });
721
+ if (!res.ok) {
722
+ const err = await res.json().catch(() => ({ message: res.statusText }));
723
+ throw new Error(err.message || `Failed to replace relations for ${this.typeName}/${id}`);
724
+ }
725
+ return res.json().catch(() => ({ success: true }));
726
+ }
727
+ /**
728
+ * Remove one graph link between two records
729
+ */
730
+ async unlink(id, relationName, targetId, targetTypeName) {
731
+ const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/relations/${encodeURIComponent(relationName)}/${encodeURIComponent(targetId)}?targetTypeName=${encodeURIComponent(targetTypeName)}`;
732
+ const res = await this.fetchFn(url, {
733
+ method: "DELETE",
734
+ headers: this.getHeaders()
735
+ });
736
+ if (!res.ok) {
737
+ const err = await res.json().catch(() => ({ message: res.statusText }));
738
+ throw new Error(err.message || `Failed to delete relation ${relationName} for ${this.typeName}/${id}`);
739
+ }
740
+ return res.json().catch(() => ({ success: true }));
741
+ }
742
+ /**
743
+ * Execute a type-level workflow plugin action (e.g. checkout, batch-process)
744
+ */
745
+ async action(actionName, payload) {
746
+ const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/actions/${actionName}`;
747
+ const res = await this.fetchFn(url, {
748
+ method: "POST",
749
+ headers: this.getHeaders(),
750
+ body: payload ? JSON.stringify(payload) : void 0
751
+ });
752
+ if (!res.ok) {
753
+ const err = await res.json().catch(() => ({ message: res.statusText }));
754
+ throw new Error(err.message || `Action ${actionName} failed on type ${this.typeName}`);
755
+ }
756
+ return res.json();
757
+ }
758
+ /**
759
+ * Execute an instance-level workflow plugin action (e.g. cancel order, approve)
760
+ */
761
+ async instanceAction(id, actionName, payload) {
762
+ const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/${id}/actions/${actionName}`;
763
+ const res = await this.fetchFn(url, {
764
+ method: "POST",
765
+ headers: this.getHeaders(),
766
+ body: payload ? JSON.stringify(payload) : void 0
767
+ });
768
+ if (!res.ok) {
769
+ const err = await res.json().catch(() => ({ message: res.statusText }));
770
+ throw new Error(err.message || `Action ${actionName} failed on ${this.typeName}/${id}`);
771
+ }
772
+ return res.json();
773
+ }
774
+ /**
775
+ * Export matching entity records to CSV format with active filters
776
+ */
777
+ async exportCsv() {
778
+ const qs = this.queryParams.toString();
779
+ const url = `${this.gatewayUrl}/generic/${this.tenantId}/${this.typeName}/export/csv${qs ? `?${qs}` : ""}`;
780
+ const res = await this.fetchFn(url, {
781
+ headers: {
782
+ ...this.getHeaders(),
783
+ "Accept": "text/csv"
784
+ }
785
+ });
786
+ if (!res.ok) {
787
+ const err = await res.json().catch(() => ({ message: res.statusText }));
788
+ throw new Error(err.message || `Failed to export CSV for ${this.typeName}`);
789
+ }
790
+ return res.text();
791
+ }
487
792
  };
488
793
 
489
794
  // src/client.ts
@@ -538,6 +843,27 @@ var Zone4CodeClient = class {
538
843
  entities(typeName) {
539
844
  return this.from(typeName);
540
845
  }
846
+ /**
847
+ * Get current authenticated user profile and platform wallet
848
+ */
849
+ async getMe() {
850
+ const token = this.auth.getToken();
851
+ if (!token) {
852
+ throw new Error("Not authenticated: please call login() or setToken() first");
853
+ }
854
+ const res = await this.fetchFn(`${this.gatewayUrl}/generic/${this.tenantId}/me`, {
855
+ headers: {
856
+ "Content-Type": "application/json",
857
+ "Authorization": `Bearer ${token}`,
858
+ "x-tenant-id": this.tenantId
859
+ }
860
+ });
861
+ if (!res.ok) {
862
+ const err = await res.json().catch(() => ({ message: res.statusText }));
863
+ throw new Error(err.message || `Failed to fetch profile & wallet (HTTP ${res.status})`);
864
+ }
865
+ return res.json();
866
+ }
541
867
  /**
542
868
  * Check gateway connectivity & health
543
869
  */