@merkaly/nuxt 0.9.3 → 0.10.0

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/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.14.0"
6
6
  },
7
- "version": "0.9.3",
7
+ "version": "0.10.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { useRoute } from "#app";
3
- import { useAuth, watchOnce, addRouteMiddleware, useNuxtApp } from "#imports";
3
+ import { computed } from "vue";
4
+ import { useAuth, useTenant, watchOnce, addRouteMiddleware, useNuxtApp } from "#imports";
4
5
  import AuthMiddleware from "../middleware/auth";
5
6
  import { useNavigation } from "../composables/useNavigation";
6
7
  import AuthError from "./auth/AuthError.vue";
@@ -8,8 +9,10 @@ import { useColorMode } from "@vueuse/core";
8
9
  import { Notivue, NotificationProgress, Notification, pastelTheme } from "notivue";
9
10
  const $route = useRoute();
10
11
  const { clear, error, isLoading, logout, retry } = useAuth();
12
+ const { fromServer } = useTenant();
11
13
  const { hook } = useNuxtApp();
12
14
  useColorMode({ attribute: "data-bs-theme" });
15
+ const blocking = computed(() => isLoading.value && !fromServer.value);
13
16
  watchOnce(isLoading, () => AuthMiddleware($route, $route));
14
17
  const { defer, regenerate } = useNavigation();
15
18
  addRouteMiddleware("navigation", (to) => defer(to), { global: true });
@@ -27,7 +30,7 @@ hook("page:finish", () => regenerate());
27
30
  </Notivue>
28
31
 
29
32
  <BApp>
30
- <slot v-if="isLoading" name="loading" />
33
+ <slot v-if="blocking" name="loading" />
31
34
 
32
35
  <slot v-else-if="error" :clear="clear" :error="error" :logout="logout" :retry="retry" name="error">
33
36
  <AuthError :error="error" @logout="logout" @retry="retry" />
@@ -81,8 +81,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
81
81
  }>, {
82
82
  mode: PlaceTypes;
83
83
  disabled: boolean;
84
- countries: string[];
85
84
  placeholder: string;
85
+ countries: string[];
86
86
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
87
87
  declare const _default: typeof __VLS_export;
88
88
  export default _default;
@@ -81,8 +81,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
81
81
  }>, {
82
82
  mode: PlaceTypes;
83
83
  disabled: boolean;
84
- countries: string[];
85
84
  placeholder: string;
85
+ countries: string[];
86
86
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
87
87
  declare const _default: typeof __VLS_export;
88
88
  export default _default;
@@ -1,5 +1,5 @@
1
- import { reactive, ref } from "vue";
2
- import { useNuxtApp } from "#imports";
1
+ import { getCurrentInstance, onServerPrefetch, reactive, ref } from "vue";
2
+ import { useNuxtApp, useState } from "#imports";
3
3
  function useApi(callback) {
4
4
  const { $api } = useNuxtApp();
5
5
  const controller = new AbortController();
@@ -11,13 +11,17 @@ function useApi(callback) {
11
11
  return options;
12
12
  };
13
13
  const initialOptions = getOptions(params);
14
+ const nuxtApp = useNuxtApp();
15
+ const ssr = import.meta.server || nuxtApp.payload.serverRendered === true;
16
+ const key = `merkaly:api:${initialOptions.method}:${initialOptions.uri}:${JSON.stringify(initialOptions.query ?? {})}`;
14
17
  const loading = ref(false);
15
- const data = ref(initialOptions.default?.());
16
- const meta = ref({});
18
+ const data = ssr ? useState(`${key}:data`, () => initialOptions.default?.()) : ref(initialOptions.default?.());
19
+ const meta = ssr ? useState(`${key}:meta`, () => ({})) : ref({});
20
+ const prefetched = ssr ? useState(`${key}:prefetched`, () => false) : ref(false);
17
21
  const error = ref();
18
22
  const execute = async (args = {}) => {
19
- Object.keys(args).forEach((key) => {
20
- params[key] = args[key];
23
+ Object.keys(args).forEach((key2) => {
24
+ params[key2] = args[key2];
21
25
  });
22
26
  const currentOptions = getOptions(params);
23
27
  await $api(currentOptions.uri, {
@@ -32,7 +36,11 @@ function useApi(callback) {
32
36
  });
33
37
  };
34
38
  if (initialOptions.immediate) {
35
- void execute();
39
+ if (import.meta.server && getCurrentInstance()) {
40
+ onServerPrefetch(() => execute().then(() => void (prefetched.value = true)));
41
+ } else if (!(nuxtApp.isHydrating && prefetched.value)) {
42
+ void execute();
43
+ }
36
44
  }
37
45
  return {
38
46
  abort: () => controller.abort(),
@@ -1,5 +1,8 @@
1
1
  export declare const useTenant: () => {
2
+ fromServer: import("vue").Ref<boolean, boolean>;
2
3
  isReady: import("vue").ComputedRef<boolean>;
3
- setTenant: (value: string) => string;
4
+ setTenant: (value: string, options?: {
5
+ server?: boolean;
6
+ }) => void;
4
7
  tenant: import("vue").Ref<string, string>;
5
8
  };
@@ -2,9 +2,14 @@ import { computed } from "vue";
2
2
  import { useState } from "#imports";
3
3
  export const useTenant = () => {
4
4
  const tenant = useState("tenant:id", () => "");
5
+ const fromServer = useState("tenant:server", () => false);
5
6
  const isReady = computed(() => Boolean(tenant.value));
6
- const setTenant = (value) => tenant.value = value;
7
+ const setTenant = (value, options = {}) => {
8
+ tenant.value = value;
9
+ if (options.server) fromServer.value = true;
10
+ };
7
11
  return {
12
+ fromServer,
8
13
  isReady,
9
14
  setTenant,
10
15
  tenant
@@ -1,66 +1,78 @@
1
- import { defineNuxtPlugin, useRuntimeConfig } from "#app";
1
+ import { defineNuxtPlugin, useRequestURL, useRuntimeConfig } from "#app";
2
2
  import { useAuth, useTenant } from "#imports";
3
3
  const GLOBAL_API_HEADER = "x-merkaly-global";
4
- export default defineNuxtPlugin(({ provide }) => provide("api", async (url, options = {}) => {
5
- const { token } = useAuth();
6
- const { tenant } = useTenant();
7
- const { public: { merkaly } } = useRuntimeConfig();
8
- const requiresTenant = options.requiresTenant ?? merkaly.auth0.requiresTenant;
9
- if (!tenant.value && !options.global && requiresTenant) {
10
- throw new Error(
11
- `[api] Tenant context is required but not set for: ${url}
4
+ function serverBaseURL(api, global) {
5
+ const prefix = global ? "" : (api.prefix ?? "").replace(/^\/+|\/+$/g, "");
6
+ return prefix ? `${api.url.replace(/\/+$/, "")}/${prefix}` : api.url;
7
+ }
8
+ export default defineNuxtPlugin(({ provide }) => {
9
+ const requestHost = import.meta.server ? useRequestURL().host : void 0;
10
+ if (import.meta.server && import.meta.dev && new URL(useRuntimeConfig().public.merkaly.api.url).hostname.endsWith(".test")) {
11
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
12
+ }
13
+ provide("api", async (url, options = {}) => {
14
+ const { token } = useAuth();
15
+ const { tenant } = useTenant();
16
+ const { public: { merkaly } } = useRuntimeConfig();
17
+ const requiresTenant = options.requiresTenant ?? merkaly.auth0.requiresTenant;
18
+ if (!tenant.value && !options.global && requiresTenant) {
19
+ throw new Error(
20
+ `[api] Tenant context is required but not set for: ${url}
12
21
  This surface has requiresTenant: true. Possible causes:
13
22
  1. The request fired before the auth boot sequence completed (race condition).
14
23
  2. setTenant() was never called in this surface's auth plugin.
15
24
  3. The adapter should use { global: true } to bypass tenant enforcement.`
16
- );
17
- }
18
- await $fetch(url, {
19
- baseURL: "/api",
20
- body: options?.body,
21
- headers: {
22
- authorization: token.value ? `Bearer ${token.value}` : "",
23
- identity: tenant.value,
24
- [GLOBAL_API_HEADER]: options.global ? "true" : "",
25
- ...options?.headers
26
- },
27
- method: options?.method,
28
- onRequest: async () => {
29
- if (options.data) options.data.value = options.default?.();
30
- if (options.meta) options.meta.value = {};
31
- if (options.error) options.error.value = void 0;
32
- if (options.loading) options.loading.value = true;
33
- const result = await Promise.resolve(options?.onBeforeSend?.({ body: options.body, headers: options.headers, query: options.query })).then((res) => res).catch(() => false);
34
- if (result === false) {
35
- options.controller?.abort?.("Request aborted by onBeforeSend");
36
- }
37
- },
38
- onRequestError: async ({ error, response, request }) => {
39
- await options.onFatal?.(error);
40
- if (options.error) options.error.value = error;
41
- await options?.onComplete?.({ response, request });
42
- if (options.loading) options.loading.value = false;
43
- },
44
- onResponse: async ({ response, request }) => {
45
- const { status, _data, headers } = response;
46
- const { data, meta } = _data || {};
47
- await options?.onResponse?.({ response, request });
48
- if (status >= 400) return;
49
- await options?.onSuccess?.({ data, meta, headers, params: options.params });
50
- if (options.data) options.data.value = data;
51
- if (options.meta) options.meta.value = meta;
52
- await options?.onComplete?.({ response, request });
53
- if (options.loading) options.loading.value = false;
54
- },
55
- onResponseError: async ({ response, request }) => {
56
- const { _data } = response;
57
- await options.onError?.(_data, { response, request });
58
- if (options.error) options.error.value = _data;
59
- await options?.onComplete?.({ response, request });
60
- if (options.loading) options.loading.value = false;
61
- },
62
- query: options?.query,
63
- retry: false,
64
- signal: options.controller?.signal
65
- }).catch((reason) => reason);
66
- }));
25
+ );
26
+ }
27
+ await $fetch(url, {
28
+ baseURL: import.meta.server ? serverBaseURL(merkaly.api, options.global) : "/api",
29
+ body: options?.body,
30
+ headers: {
31
+ authorization: token.value ? `Bearer ${token.value}` : "",
32
+ identity: tenant.value,
33
+ [GLOBAL_API_HEADER]: options.global ? "true" : "",
34
+ ...requestHost ? { "x-merkaly-host": requestHost } : {},
35
+ // tenant host, as the proxy sends it
36
+ ...options?.headers
37
+ },
38
+ method: options?.method,
39
+ onRequest: async () => {
40
+ if (options.data) options.data.value = options.default?.();
41
+ if (options.meta) options.meta.value = {};
42
+ if (options.error) options.error.value = void 0;
43
+ if (options.loading) options.loading.value = true;
44
+ const result = await Promise.resolve(options?.onBeforeSend?.({ body: options.body, headers: options.headers, query: options.query })).then((res) => res).catch(() => false);
45
+ if (result === false) {
46
+ options.controller?.abort?.("Request aborted by onBeforeSend");
47
+ }
48
+ },
49
+ onRequestError: async ({ error, response, request }) => {
50
+ await options.onFatal?.(error);
51
+ if (options.error) options.error.value = error;
52
+ await options?.onComplete?.({ response, request });
53
+ if (options.loading) options.loading.value = false;
54
+ },
55
+ onResponse: async ({ response, request }) => {
56
+ const { status, _data, headers } = response;
57
+ const { data, meta } = _data || {};
58
+ await options?.onResponse?.({ response, request });
59
+ if (status >= 400) return;
60
+ await options?.onSuccess?.({ data, meta, headers, params: options.params });
61
+ if (options.data) options.data.value = data;
62
+ if (options.meta) options.meta.value = meta;
63
+ await options?.onComplete?.({ response, request });
64
+ if (options.loading) options.loading.value = false;
65
+ },
66
+ onResponseError: async ({ response, request }) => {
67
+ const { _data } = response;
68
+ await options.onError?.(_data, { response, request });
69
+ if (options.error) options.error.value = _data;
70
+ await options?.onComplete?.({ response, request });
71
+ if (options.loading) options.loading.value = false;
72
+ },
73
+ query: options?.query,
74
+ retry: false,
75
+ signal: options.controller?.signal
76
+ }).catch((reason) => reason);
77
+ });
78
+ });
@@ -3,9 +3,13 @@ import type { Auth0Client, User } from '@auth0/auth0-spa-js';
3
3
  import type { ApiOptions } from '../plugins/api.global';
4
4
  import type { AuthErrorContext } from '../composables/useAuth';
5
5
 
6
+ type MerkalyAuth0Client = Auth0Client & {
7
+ linkWithConnection: (connection: string) => Promise<unknown>; // added by the auth0 plugin
8
+ };
9
+
6
10
  declare module '#app' {
7
11
  interface NuxtApp {
8
- $auth0: Auth0Client;
12
+ $auth0: MerkalyAuth0Client;
9
13
  $api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T>;
10
14
  $gmap: never;
11
15
  }
@@ -20,7 +24,7 @@ declare module '#app' {
20
24
 
21
25
  declare module 'vue' {
22
26
  interface ComponentCustomProperties {
23
- $auth0: Auth0Client;
27
+ $auth0: MerkalyAuth0Client;
24
28
  $api: <T = unknown>(url: string, options?: ApiOptions) => Promise<T>;
25
29
  $gmap: never;
26
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkaly/nuxt",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/merkaly-io/nuxt.git"