@hostlink/nuxt-light 1.75.0 → 1.77.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
@@ -55,6 +55,135 @@ export default defineNuxtConfig({
55
55
 
56
56
  That's it! You can now use nuxt-light in your Nuxt app ✨
57
57
 
58
+ ## API clients
59
+
60
+ `nuxt-light` supports multiple Light API endpoints. The client named `auth` is
61
+ always the default: when a component or composable does not specify a client,
62
+ the request is sent to `auth`.
63
+
64
+ Configure named clients in `nuxt.config.ts`:
65
+
66
+ ```ts
67
+ export default defineNuxtConfig({
68
+ runtimeConfig: {
69
+ public: {
70
+ light: {
71
+ clients: {
72
+ auth: {
73
+ baseURL: 'https://auth.example.com/graphql',
74
+ },
75
+ business: {
76
+ baseURL: 'https://business.example.com/graphql',
77
+ audience: 'business-api',
78
+ },
79
+ infra: {
80
+ baseURL: 'https://infra.example.com/graphql',
81
+ audience: 'infra-api',
82
+ },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ modules: [
88
+ '@hostlink/nuxt-light',
89
+ ],
90
+ })
91
+ ```
92
+
93
+ A client can also be configured with a URL string:
94
+
95
+ ```ts
96
+ clients: {
97
+ auth: '/auth-api/',
98
+ business: '/business-api/',
99
+ }
100
+ ```
101
+
102
+ Clients with an `audience` obtain a short-lived audience token lazily from the
103
+ default `auth` client. The token is cached by that client, refreshed shortly
104
+ before expiry, and sent as a Bearer token automatically:
105
+
106
+ ```http
107
+ Authorization: Bearer <audience-access-token>
108
+ ```
109
+
110
+ Components and composables do not need to fetch or attach tokens themselves.
111
+ For example, the first request made by this table automatically requests a
112
+ `business-api` token from Auth:
113
+
114
+ ```vue
115
+ <L-Table client="business" model-name="Order" />
116
+ ```
117
+
118
+ ### Backwards-compatible single endpoint
119
+
120
+ The existing `public.apiBase` setting is still supported. When no named
121
+ `auth` client is configured, `apiBase` is registered as the `auth` client:
122
+
123
+ ```ts
124
+ export default defineNuxtConfig({
125
+ runtimeConfig: {
126
+ public: {
127
+ apiBase: '/api/',
128
+ },
129
+ },
130
+ })
131
+ ```
132
+
133
+ If neither setting is provided, the default Auth API URL is `/api/`.
134
+
135
+ ### Selecting a client
136
+
137
+ Use `useLightClient()` without an argument to get the Auth API client:
138
+
139
+ ```ts
140
+ const auth = useLightClient()
141
+ const business = useLightClient('business')
142
+
143
+ await auth.auth.login(username, password)
144
+ await business.query({
145
+ app: {
146
+ orders: {
147
+ order_id: true,
148
+ },
149
+ },
150
+ })
151
+ ```
152
+
153
+ The standard composables also default to `auth`:
154
+
155
+ ```ts
156
+ await q(query) // auth
157
+ await q(query, 'business') // business
158
+
159
+ model('User') // auth
160
+ model('Order', 'business') // business
161
+
162
+ collect('User', fields) // auth
163
+ collect('Order', fields, 'business') // business
164
+
165
+ await m('updateUser', args, fields) // auth
166
+ await m('updateOrder', args, fields, 'business') // business
167
+ ```
168
+
169
+ `<L-Table>` uses `auth` unless its `client` prop is provided:
170
+
171
+ ```vue
172
+ <template>
173
+ <!-- Uses the default auth client -->
174
+ <L-Table model-name="User" />
175
+
176
+ <!-- Uses the business client -->
177
+ <L-Table
178
+ client="business"
179
+ model-name="Order"
180
+ />
181
+ </template>
182
+ ```
183
+
184
+ An unknown client name throws an explicit configuration error instead of
185
+ silently sending the request to another endpoint.
186
+
58
187
  ## Development
59
188
 
60
189
  ```bash
@@ -119,4 +248,4 @@ nuxt.config.ts
119
248
  ]
120
249
  }
121
250
  }
122
- ```
251
+ ```
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.75.0",
4
+ "version": "1.77.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -220,7 +220,6 @@ const module$1 = defineNuxtModule({
220
220
  });
221
221
  addImports({ name: "isGranted", from: "@hostlink/light" });
222
222
  addImports({ name: "getGrantedRights", from: "@hostlink/light" });
223
- addImports({ name: "query", as: "q", from: "@hostlink/light" });
224
223
  addImports({ name: "mutation", from: "@hostlink/light" });
225
224
  addImports({ name: "query", from: "@hostlink/light" });
226
225
  await addComponentsDir({
@@ -5,7 +5,7 @@ import { useQuasar, setCssVar, getCssVar } from "quasar";
5
5
  import { useI18n } from "vue-i18n";
6
6
  import { ref, computed, reactive, provide, watch, toRaw, onMounted } from "vue";
7
7
  import { useRuntimeConfig } from "nuxt/app";
8
- import { logout } from "@hostlink/light";
8
+ import useLightClient from "../../composables/useLightClient";
9
9
  import { filterMenuItems } from "../../utils/filterMenuItems";
10
10
  const emits = defineEmits(["logout"]);
11
11
  defineProps({
@@ -249,7 +249,7 @@ if (route.fullPath == "/" && my.default_page) {
249
249
  router.push(my.default_page);
250
250
  }
251
251
  const onLogout = async () => {
252
- await logout();
252
+ await useLightClient().auth.logout();
253
253
  emits("logout");
254
254
  };
255
255
  </script>
@@ -2,7 +2,7 @@
2
2
  import { ref, reactive, onMounted, resolveComponent } from "vue";
3
3
  import { useQuasar } from "quasar";
4
4
  import { useHead } from "#imports";
5
- import { getApiClient, changeExpiredPassword, login } from "@hostlink/light";
5
+ import { getApiClient } from "@hostlink/light";
6
6
  const api = getApiClient();
7
7
  import { useI18n } from "vue-i18n";
8
8
  const { t } = useI18n();
@@ -42,7 +42,7 @@ const passwordExpiredProcess = (username, password) => {
42
42
  persistent: true
43
43
  }).onOk(async (newPassword) => {
44
44
  try {
45
- await changeExpiredPassword(username, password, newPassword);
45
+ await api.auth.changeExpiredPassword(username, password, newPassword);
46
46
  $q.notify({
47
47
  message: t("Your password has been changed successfully, please login again"),
48
48
  color: "positive",
@@ -81,7 +81,7 @@ const submit = async () => {
81
81
  if (await form1.value.validate()) {
82
82
  try {
83
83
  loading.value = true;
84
- await login(data.username, data.password, data.code);
84
+ await api.auth.login(data.username, data.password, data.code);
85
85
  emits("login");
86
86
  } catch (e) {
87
87
  data.code = "";
@@ -27,6 +27,7 @@ export type LTableColumn = QTableColumn & {
27
27
  autoWidth?: boolean;
28
28
  };
29
29
  export type LTableProps = QTableProps & {
30
+ client?: string;
30
31
  columns?: Array<LTableColumn>;
31
32
  actions?: Array<'view' | 'edit' | 'delete' | 'update'>;
32
33
  sortBy?: string;
@@ -127,6 +128,7 @@ declare const __VLS_base: import("vue").DefineComponent<LTableProps, {
127
128
  rowsPerPageLabel: string;
128
129
  rowsPerPageOptions: readonly any[];
129
130
  selection: "none" | "single" | "multiple";
131
+ client: string;
130
132
  searchable: boolean;
131
133
  canExpandRow: (row: any) => boolean;
132
134
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -5,8 +5,9 @@ import { ref, computed, onMounted, useSlots, watch, useAttrs } from "vue";
5
5
  import useLight from "../../composables/useLight";
6
6
  import { useLightTableDefaults } from "../../composables/useLightProps";
7
7
  import model from "../../composables/model";
8
+ import useLightClient from "../../composables/useLightClient";
8
9
  import { toQuery } from "@hostlink/light";
9
- import { list, navigateTo } from "#imports";
10
+ import { navigateTo } from "#imports";
10
11
  import { useI18n } from "vue-i18n";
11
12
  import { useStorage, useSessionStorage } from "@vueuse/core";
12
13
  import { useRoute } from "#imports";
@@ -98,6 +99,7 @@ const props = defineProps({
98
99
  "onUpdate:selected": { type: Function, required: false },
99
100
  "onUpdate:expanded": { type: Function, required: false },
100
101
  onVirtualScroll: { type: Function, required: false },
102
+ client: { type: String, required: false, default: "auth" },
101
103
  actions: { type: Array, required: false, default: () => [] },
102
104
  sortBy: { type: String, required: false },
103
105
  modelName: { type: null, required: false },
@@ -138,7 +140,7 @@ if (!saveFilters.value) {
138
140
  defineOptions({ inheritAttrs: false });
139
141
  const light = useLight();
140
142
  const vAttrs = useAttrs();
141
- const L_KEYS = ["actions", "sortBy", "modelName", "searchable", "onRequestData", "addComponent", "addComponentProps", "name", "searchStyle", "canExpandRow"];
143
+ const L_KEYS = ["client", "actions", "sortBy", "modelName", "searchable", "onRequestData", "addComponent", "addComponentProps", "name", "searchStyle", "canExpandRow"];
142
144
  const base = useLightTableDefaults(props, vAttrs, L_KEYS);
143
145
  const pagination = ref(props.pagination);
144
146
  if (props.rowsPerPageOptions[0] == 0) {
@@ -342,7 +344,7 @@ const onLocalRequest = async (p) => {
342
344
  }
343
345
  loading.value = true;
344
346
  try {
345
- let l = list(model2, localFields);
347
+ let l = useLightClient(props.client).list(model2, localFields);
346
348
  l = l.filters(localFilters);
347
349
  if (sort) {
348
350
  l = l.sort(sort);
@@ -431,7 +433,7 @@ const ss = computed(() => Object.keys(slots));
431
433
  const onDelete = async (id) => {
432
434
  if (modelName.value == null) return;
433
435
  try {
434
- await model(modelName.value).delete(id);
436
+ await model(modelName.value, props.client).delete(id);
435
437
  } catch (e) {
436
438
  $q.notify({
437
439
  message: e.message,
@@ -27,6 +27,7 @@ export type LTableColumn = QTableColumn & {
27
27
  autoWidth?: boolean;
28
28
  };
29
29
  export type LTableProps = QTableProps & {
30
+ client?: string;
30
31
  columns?: Array<LTableColumn>;
31
32
  actions?: Array<'view' | 'edit' | 'delete' | 'update'>;
32
33
  sortBy?: string;
@@ -127,6 +128,7 @@ declare const __VLS_base: import("vue").DefineComponent<LTableProps, {
127
128
  rowsPerPageLabel: string;
128
129
  rowsPerPageOptions: readonly any[];
129
130
  selection: "none" | "single" | "multiple";
131
+ client: string;
130
132
  searchable: boolean;
131
133
  canExpandRow: (row: any) => boolean;
132
134
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,2 +1,2 @@
1
- declare const _default: (name: string, fields: Object) => any;
1
+ declare const _default: (name: string, fields: Object, client?: string) => any;
2
2
  export default _default;
@@ -1,5 +1,4 @@
1
- import { getApiClient } from "@hostlink/light";
2
- export default (name, fields) => {
3
- const client = getApiClient();
4
- return client.collect(name, fields);
1
+ import useLightClient from "./useLightClient.js";
2
+ export default (name, fields, client = "auth") => {
3
+ return useLightClient(client).collect(name, fields);
5
4
  };
@@ -1,3 +1,3 @@
1
1
  export default function (operation: string, args?: {
2
2
  [key: string]: any;
3
- }, fields?: any): Promise<any>;
3
+ }, fields?: any, client?: string): Promise<any>;
@@ -1,4 +1,4 @@
1
- import { mutation } from "@hostlink/light";
1
+ import useLightClient from "./useLightClient.js";
2
2
  const removeUndefinedValues = (obj) => {
3
3
  for (const key in obj) {
4
4
  if (obj[key] instanceof File) {
@@ -13,7 +13,7 @@ const removeUndefinedValues = (obj) => {
13
13
  }
14
14
  return obj;
15
15
  };
16
- export default function(operation, args, fields = []) {
16
+ export default function(operation, args, fields = [], client = "auth") {
17
17
  if (args) {
18
18
  args = removeUndefinedValues(args);
19
19
  }
@@ -29,7 +29,7 @@ export default function(operation, args, fields = []) {
29
29
  if (Object.keys(q[operation]).length === 0) {
30
30
  q[operation] = true;
31
31
  }
32
- return mutation(q).then((res) => {
32
+ return useLightClient(client).mutation(q).then((res) => {
33
33
  return res[operation];
34
34
  });
35
35
  }
@@ -1,5 +1,5 @@
1
1
  import type { LTableColumn } from "../components/L/Table.vue.js";
2
- declare const _default: (name: string) => Omit<{
2
+ declare const _default: (name: string, client?: string) => Omit<{
3
3
  field: (f: string) => import("@hostlink/light").Field | null;
4
4
  $fields: Record<string, import("@hostlink/light").Field>;
5
5
  setDataPath(path: string): string;
@@ -1,7 +1,7 @@
1
- import { getModel } from "@hostlink/light";
1
+ import useLightClient from "./useLightClient.js";
2
2
  import defu from "defu";
3
- export default (name) => {
4
- const m = getModel(name);
3
+ export default (name, client = "auth") => {
4
+ const m = useLightClient(client).model(name);
5
5
  return defu(m, {
6
6
  columns(fields) {
7
7
  let columns = [];
@@ -0,0 +1,2 @@
1
+ import type { GraphQLQuery } from '@hostlink/light';
2
+ export default function q(query: GraphQLQuery, client?: string): Promise<any>;
@@ -0,0 +1,4 @@
1
+ import useLightClient from "./useLightClient.js";
2
+ export default function q(query, client = "auth") {
3
+ return useLightClient(client).query(query);
4
+ }
@@ -0,0 +1,4 @@
1
+ import type { LightClient } from '@hostlink/light';
2
+ export declare function registerLightClient(name: string, client: LightClient): void;
3
+ export declare function clearLightClients(): void;
4
+ export default function useLightClient(name?: string): LightClient;
@@ -0,0 +1,12 @@
1
+ const clients = /* @__PURE__ */ new Map();
2
+ export function registerLightClient(name, client) {
3
+ clients.set(name, client);
4
+ }
5
+ export function clearLightClients() {
6
+ clients.clear();
7
+ }
8
+ export default function useLightClient(name = "auth") {
9
+ const client = clients.get(name);
10
+ if (!client) throw new Error(`Light client "${name}" is not configured`);
11
+ return client;
12
+ }
@@ -2,6 +2,7 @@
2
2
  import { useRouter, useRoute } from "vue-router";
3
3
  import { useQuasar } from "quasar";
4
4
  import { computed, model } from "#imports";
5
+ import { getFormClient } from "../utils/formClient";
5
6
  const route = useRoute();
6
7
  const router = useRouter();
7
8
  const $q = useQuasar();
@@ -24,6 +25,7 @@ if (props.context.attrs.onSubmitted) {
24
25
  }
25
26
  const modelName = props.context.modelName || (typeof route.name === "string" ? route.name.split("-")[0] : void 0);
26
27
  const id = props.context.modelId || route.params[typeof route.name === "string" ? route.name.split("-")[1] : "id"];
28
+ const client = getFormClient(props.context);
27
29
  if (!props.context.onSubmit) {
28
30
  props.context.node.props.onSubmit = async function() {
29
31
  const removeUndefined = (obj) => {
@@ -51,7 +53,7 @@ if (!props.context.onSubmit) {
51
53
  const v = removeUndefined(props.context.value);
52
54
  try {
53
55
  if (id) {
54
- if (await model(modelName).update(Number(id), v)) {
56
+ if (await model(modelName, client).update(Number(id), v)) {
55
57
  $q.notify({
56
58
  message: "Updated successfully",
57
59
  color: "positive",
@@ -62,7 +64,7 @@ if (!props.context.onSubmit) {
62
64
  }
63
65
  }
64
66
  } else {
65
- if (await model(modelName).add(v)) {
67
+ if (await model(modelName, client).add(v)) {
66
68
  $q.notify({
67
69
  message: "Added successfully",
68
70
  color: "positive",
@@ -117,7 +117,8 @@ export const createLightPlugin = () => {
117
117
  "layout",
118
118
  "bordered",
119
119
  "modelName",
120
- "modelId"
120
+ "modelId",
121
+ "client"
121
122
  ],
122
123
  features: [forms, disablesChildren]
123
124
  });
@@ -12,10 +12,27 @@ import { createLightPlugin } from "./formkit/index.js";
12
12
  import { plugin, defaultConfig } from "@formkit/vue";
13
13
  import getApiBase from "./composables/getApiBase.js";
14
14
  import useLight from "./composables/useLight.js";
15
+ import useLightClient, { clearLightClients, registerLightClient } from "./composables/useLightClient.js";
15
16
  import { zhTW } from "@formkit/i18n";
16
17
  export default defineNuxtPlugin((nuxtApp) => {
17
- const client = createClient(getApiBase());
18
- setApiClient(client);
18
+ const runtimeConfig = useRuntimeConfig();
19
+ const lightConfig = runtimeConfig.public.light;
20
+ const configuredClients = lightConfig?.clients ?? {};
21
+ clearLightClients();
22
+ for (const [name, config] of Object.entries(configuredClients)) {
23
+ const baseURL = typeof config === "string" ? config : config.baseURL;
24
+ if (!baseURL) throw new Error(`Light client "${name}" requires a baseURL`);
25
+ registerLightClient(name, createClient(baseURL));
26
+ }
27
+ if (!configuredClients.auth) {
28
+ registerLightClient("auth", createClient(getApiBase()));
29
+ }
30
+ const authClient = useLightClient();
31
+ setApiClient(authClient);
32
+ for (const [name, config] of Object.entries(configuredClients)) {
33
+ if (name === "auth" || typeof config === "string" || !config.audience) continue;
34
+ useLightClient(name).useAudience(authClient, config.audience);
35
+ }
19
36
  defineModel("Permission", {}).setDataPath("app.listPermission");
20
37
  defineModel("SystemValue", {}).setDataPath("app.listSystemValue");
21
38
  defineModel("Config", {}).setDataPath("app.listConfig");
@@ -0,0 +1,4 @@
1
+ export interface FormClientContext {
2
+ client?: string;
3
+ }
4
+ export declare function getFormClient(context: FormClientContext): string;
@@ -0,0 +1,3 @@
1
+ export function getFormClient(context) {
2
+ return context.client || "auth";
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostlink/nuxt-light",
3
- "version": "1.75.0",
3
+ "version": "1.77.2",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "@formkit/inputs": "^2.1.2",
39
39
  "@formkit/validation": "^2.1.2",
40
40
  "@formkit/vue": "^2.1.2",
41
- "@hostlink/light": "^3.2.7",
41
+ "@hostlink/light": "^3.4.0",
42
42
  "@nuxt/module-builder": "^1.0.1",
43
43
  "@quasar/extras": "^2.0.2",
44
44
  "@quasar/quasar-ui-qmarkdown": "^3.0.1",