@nauticana/sail 0.9.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nauticana/sail",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Metadata-driven Angular component library for CRUD admin frontends backed by the keel Go backend. Tables, forms, navigation, OTP/2FA/social auth, billing, payout onboarding.",
5
5
  "main": "src/index.ts",
6
6
  "files": [
@@ -46,7 +46,7 @@ export class TableLookup extends BaseView implements OnInit {
46
46
  for (const api in data.Apis) {
47
47
  const entry = data.Apis[api];
48
48
  if (entry.Table.TableName === tableName) {
49
- this.apiName.set(entry.Version ? entry.Version + '/' + api : api);
49
+ this.apiName.set(api); // bare RestUri; BackendService.crudUrl resolves the version
50
50
  break;
51
51
  }
52
52
  }
@@ -557,7 +557,7 @@ export abstract class BaseAuthService extends BaseRestService {
557
557
  if (!page.ItemId) continue;
558
558
  const restUri = page.RestUri ?? '';
559
559
  const api = apis?.[restUri];
560
- const apiName = api?.Version ? api.Version + '/' + restUri : restUri;
560
+ const apiName = restUri; // bare RestUri; BackendService.crudUrl resolves the version
561
561
  const tableName = api?.Table?.TableName ?? restUri;
562
562
 
563
563
  const overrideComponent = this.getRouteOverride(restUri);
@@ -1,12 +1,14 @@
1
1
  import { HttpErrorResponse, HttpParams } from "@angular/common/http";
2
- import { Injectable } from "@angular/core";
3
- import { catchError, map, Observable, throwError } from "rxjs";
4
- import { PaginatedList } from "../model/appdata";
2
+ import { inject, Injectable } from "@angular/core";
3
+ import { catchError, map, Observable, switchMap, take, throwError } from "rxjs";
4
+ import { ApplicationData, PaginatedList } from "../model/appdata";
5
5
  import { RestURL } from "./rest_url";
6
+ import { BaseAuthService } from "./auth.service";
6
7
  import { BaseRestService } from "./base_rest.service";
7
8
 
8
9
  @Injectable({providedIn: 'root'})
9
10
  export class BackendService extends BaseRestService {
11
+ private readonly auth = inject(BaseAuthService);
10
12
  protected readonly prefix = this.url(RestURL.api_prefix);
11
13
 
12
14
  protected handleError(error: HttpErrorResponse) {
@@ -18,33 +20,25 @@ export class BackendService extends BaseRestService {
18
20
  return throwError(() => 'error occurred, see console log and try again');
19
21
  }
20
22
 
21
- /**
22
- * Build a fully-qualified URL under the configured API base. Use for
23
- * endpoints that don't fit the metadata-driven `/api/{name}/{op}`
24
- * shape — e.g. domain-specific actions like `rides/start`. Strips any
25
- * leading slash on `path` so callers can pass either form.
26
- *
27
- * For metadata-driven CRUD use list/get/post/delete instead — they
28
- * already build the URL.
29
- */
23
+ /** Free-form path under the API base — for custom endpoints (e.g. `rides/start`). CRUD methods use `crudUrl` instead. */
30
24
  protected apiUrl(path: string): string {
31
25
  const trimmed = path.startsWith('/') ? path.slice(1) : path;
32
26
  return this.prefix + trimmed;
33
27
  }
34
28
 
29
+ /** CRUD URL versioned from the backend (Apis[apiName].Version); throws rather than assume a version. */
30
+ private crudUrl(apiName: string, action: string): string {
31
+ const version = this.auth.getApiDictionary(apiName)?.Version;
32
+ if (!version) {
33
+ throw new Error(`sail: no backend version for API "${apiName}"; refusing to assume one.`);
34
+ }
35
+ return `${this.prefix}${version}/${apiName}/${action}`;
36
+ }
37
+
35
38
  /**
36
39
  * Generic typed HTTP request for endpoints that don't fit the
37
40
  * list/get/post/delete pattern. Subclasses use this for
38
- * domain-specific actions:
39
- *
40
- * class RideService extends BackendService {
41
- * startRide(req: StartRideReq) {
42
- * return this.request<RideResponse>('POST', 'rides/start', req);
43
- * }
44
- * }
45
- *
46
- * Errors are routed through handleError so callers see the same
47
- * "see console log and try again" envelope as CRUD methods.
41
+ * domain-specific actions.
48
42
  */
49
43
  protected request<T>(method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', path: string, body?: unknown): Observable<T> {
50
44
  return this.http.request<T>(method, this.apiUrl(path), { body }).pipe(catchError(this.handleError));
@@ -62,34 +56,45 @@ export class BackendService extends BaseRestService {
62
56
  return params;
63
57
  }
64
58
 
65
- /**
66
- * keel list endpoints return `{items, limit, offset, total}`. This method
67
- * unwraps to the array — most callers only need the records. Use
68
- * `listPaginated()` to access the total + offset. Pass `_limit` / `_offset`
69
- * in `filter` to control paging (defaults: 100 / 0).
70
- */
59
+ /** Gates a CRUD call on appdata being loaded — `appData$` is a ReplaySubject(1) so this resolves synchronously once the cache is populated, and blocks the call (instead of throwing) when a component fires CRUD before loadAppData() finishes. */
60
+ private appDataReady(): Observable<ApplicationData> {
61
+ return this.auth.getAppData().pipe(take(1));
62
+ }
63
+
64
+ /** Unwraps `{items, limit, offset, total}` to the items array. Pass `_limit` / `_offset` in `filter` to control paging (defaults: 100 / 0). */
71
65
  list<T>(apiName: string, filter?: {[key: string]: string}) : Observable<T[]> {
72
66
  return this.listPaginated<T>(apiName, filter).pipe(map((page) => page.items));
73
67
  }
74
68
 
75
- /** Paginated list — returns `{items, limit, offset, total}`. */
76
69
  listPaginated<T>(apiName: string, filter?: {[key: string]: string}) : Observable<PaginatedList<T>> {
77
- return this.http.get<PaginatedList<T>>(
78
- this.apiUrl(apiName + '/list'),
79
- {params: this.toParams(filter)},
80
- ).pipe(catchError(this.handleError));
70
+ return this.appDataReady().pipe(
71
+ switchMap(() => this.http.get<PaginatedList<T>>(
72
+ this.crudUrl(apiName, 'list'),
73
+ {params: this.toParams(filter)},
74
+ )),
75
+ catchError(this.handleError),
76
+ );
81
77
  }
82
78
 
83
79
  get<T>(apiName: string, filter?: {[key: string]: string}) : Observable<T> {
84
- return this.http.get<T>(this.apiUrl(apiName + '/get'), {params: this.toParams(filter)}).pipe(catchError(this.handleError));
80
+ return this.appDataReady().pipe(
81
+ switchMap(() => this.http.get<T>(this.crudUrl(apiName, 'get'), {params: this.toParams(filter)})),
82
+ catchError(this.handleError),
83
+ );
85
84
  }
86
85
 
87
86
  post<T>(apiName: string, items: T | T[]) : Observable<{message: string}> {
88
- return this.http.post<{message: string}>(this.apiUrl(apiName + '/post'), items).pipe(catchError(this.handleError));
87
+ return this.appDataReady().pipe(
88
+ switchMap(() => this.http.post<{message: string}>(this.crudUrl(apiName, 'post'), items)),
89
+ catchError(this.handleError),
90
+ );
89
91
  }
90
92
 
91
93
  delete(apiName: string, filter?: {[key: string]: string}): Observable<{message: string}> {
92
- return this.http.delete<{message: string}>(this.apiUrl(apiName + '/delete'), {params: this.toParams(filter)}).pipe(catchError(this.handleError));
94
+ return this.appDataReady().pipe(
95
+ switchMap(() => this.http.delete<{message: string}>(this.crudUrl(apiName, 'delete'), {params: this.toParams(filter)})),
96
+ catchError(this.handleError),
97
+ );
93
98
  }
94
99
 
95
100
  /**