@bg-dev/nuxt-zenstack 0.0.7 → 0.0.8
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 +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/composables/index.d.ts +1 -0
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/useZenstackRead/index.d.ts +1 -1
- package/dist/runtime/composables/useZenstackReadMany/index.d.ts +3 -7
- package/dist/runtime/composables/useZenstackReadMany/index.js +12 -5
- package/dist/runtime/composables/useZenstackRealtime/index.d.ts +1 -1
- package/dist/runtime/composables/useZenstackRealtime/index.js +23 -14
- package/dist/runtime/composables/useZenstackStore/index.d.ts +4 -3
- package/dist/runtime/composables/useZenstackStore/index.js +11 -7
- package/dist/runtime/composables/useZenstackStore/normalization.d.ts +31 -2
- package/dist/runtime/composables/useZenstackStore/normalization.js +151 -4
- package/dist/runtime/server/api/models/[model]/[id].delete.d.ts +1 -3
- package/dist/runtime/server/api/models/[model]/[id].get.d.ts +1 -3
- package/dist/runtime/server/api/models/[model]/[id].patch.d.ts +1 -3
- package/dist/runtime/server/api/models/[model]/index.get.d.ts +1 -3
- package/dist/runtime/server/api/models/[model]/index.post.d.ts +1 -3
- package/dist/runtime/server/routes/realtime.js +22 -3
- package/dist/runtime/server/utils/helpers.d.ts +7 -2
- package/dist/runtime/server/utils/helpers.js +1 -1
- package/dist/runtime/server/utils/index.d.ts +2 -2
- package/dist/runtime/server/utils/index.js +2 -2
- package/dist/runtime/server/utils/operations/create.js +4 -3
- package/dist/runtime/server/utils/operations/delete.js +4 -2
- package/dist/runtime/server/utils/operations/update.js +4 -3
- package/dist/runtime/server/utils/parsers.d.ts +1 -1
- package/dist/runtime/types/template.d.ts +8 -10
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -3,4 +3,5 @@ export { useZenstackReadMany } from './useZenstackReadMany/index.js';
|
|
|
3
3
|
export { useZenstackRead } from './useZenstackRead/index.js';
|
|
4
4
|
export { useZenstackDelete } from './useZenstackDelete/index.js';
|
|
5
5
|
export { useZenstackUpdate } from './useZenstackUpdate/index.js';
|
|
6
|
+
export { useZenstackStore } from './useZenstackStore/index.js';
|
|
6
7
|
export { provideFetch as provideZenstackFetch } from './common.js';
|
|
@@ -3,4 +3,5 @@ export { useZenstackReadMany } from "./useZenstackReadMany/index.js";
|
|
|
3
3
|
export { useZenstackRead } from "./useZenstackRead/index.js";
|
|
4
4
|
export { useZenstackDelete } from "./useZenstackDelete/index.js";
|
|
5
5
|
export { useZenstackUpdate } from "./useZenstackUpdate/index.js";
|
|
6
|
+
export { useZenstackStore } from "./useZenstackStore/index.js";
|
|
6
7
|
export { provideFetch as provideZenstackFetch } from "./common.js";
|
|
@@ -5,7 +5,7 @@ type Options<Zinclude> = {
|
|
|
5
5
|
/**
|
|
6
6
|
* - `cache-first` means that the query will first try to fetch the data from the store. If the data is not found in the store, it will fetch the data from the server and update the store.
|
|
7
7
|
* - `cache-and-fetch` means that the query will first try to fetch the data from the cache. It will then fetch the data from the server and update the cache.
|
|
8
|
-
* - `fetch-only` means that the query will only fetch the data from the server. The data will be stored in the
|
|
8
|
+
* - `fetch-only` means that the query will only fetch the data from the server. The data will be stored in the store when the query is resolved.
|
|
9
9
|
* - `cache-only` means that the query will only fetch the data from the store.
|
|
10
10
|
* @default 'cache-first'
|
|
11
11
|
*/
|
|
@@ -5,7 +5,7 @@ type Options<Zinclude, Zwhere, ZorderBy> = {
|
|
|
5
5
|
/**
|
|
6
6
|
* - `cache-first` means that the query will first try to fetch the data from the store. If the data is not found in the store, it will fetch the data from the server and update the store.
|
|
7
7
|
* - `cache-and-fetch` means that the query will first try to fetch the data from the cache. It will then fetch the data from the server and update the cache.
|
|
8
|
-
* - `fetch-only` means that the query will only fetch the data from the server. The data will be stored in the
|
|
8
|
+
* - `fetch-only` means that the query will only fetch the data from the server. The data will be stored in the store when the query is resolved.
|
|
9
9
|
* - `cache-only` means that the query will only fetch the data from the store.
|
|
10
10
|
* @default 'cache-first'
|
|
11
11
|
*/
|
|
@@ -30,16 +30,11 @@ type Options<Zinclude, Zwhere, ZorderBy> = {
|
|
|
30
30
|
* @default undefined
|
|
31
31
|
*/
|
|
32
32
|
orderBy?: MaybeRefOrGetter<ZorderBy>;
|
|
33
|
-
/**
|
|
34
|
-
* The number of items to skip.
|
|
35
|
-
* @default 0
|
|
36
|
-
*/
|
|
37
|
-
skip?: MaybeRefOrGetter<number>;
|
|
38
33
|
/**
|
|
39
34
|
* The number of items to take.
|
|
40
35
|
* @default 1000
|
|
41
36
|
*/
|
|
42
|
-
take?:
|
|
37
|
+
take?: number;
|
|
43
38
|
};
|
|
44
39
|
export declare function useZenstackReadMany<Zmodel extends $Zmodel, Zinclude extends $Zinclude<Zmodel>, Zitem extends $Zitem<Zmodel, Zinclude>, Zwhere extends $Zwhere<Zmodel>, ZorderBy extends $ZorderBy<Zmodel>>(model: Zmodel, opts?: Options<Zinclude, Zwhere, ZorderBy>): Promise<{
|
|
45
40
|
data: Ref<Zitem[] | null>;
|
|
@@ -47,5 +42,6 @@ export declare function useZenstackReadMany<Zmodel extends $Zmodel, Zinclude ext
|
|
|
47
42
|
status: Ref<Status>;
|
|
48
43
|
canFetchMore: Ref<boolean>;
|
|
49
44
|
refetch: () => Promise<void>;
|
|
45
|
+
fetchMore: () => Promise<void>;
|
|
50
46
|
}>;
|
|
51
47
|
export {};
|
|
@@ -10,19 +10,20 @@ export async function useZenstackReadMany(model, opts = {}) {
|
|
|
10
10
|
const error = ref(null);
|
|
11
11
|
const status = ref("idle");
|
|
12
12
|
const canFetchMore = ref(true);
|
|
13
|
+
const skip = ref(0);
|
|
13
14
|
const store = useZenstackStore();
|
|
14
15
|
const config = getConfig();
|
|
15
16
|
const nuxtApp = useNuxtApp();
|
|
16
|
-
const watchedOptions = [opts.where, opts.orderBy
|
|
17
|
+
const watchedOptions = [opts.where, opts.orderBy].filter((option) => isRef(option) || typeof option === "function");
|
|
17
18
|
const method = "GET";
|
|
18
19
|
opts.fetchPolicy ??= config.fetchPolicy;
|
|
19
20
|
opts.immediate ??= true;
|
|
20
|
-
opts.skip ??= 0;
|
|
21
21
|
opts.take ??= 1e3;
|
|
22
22
|
if (opts.fetchPolicy !== "fetch-only")
|
|
23
23
|
updateData();
|
|
24
24
|
watch(store.state, () => updateData());
|
|
25
25
|
watch(watchedOptions, () => {
|
|
26
|
+
skip.value = 0;
|
|
26
27
|
updateData();
|
|
27
28
|
if (canFetchAutomatically())
|
|
28
29
|
refetch();
|
|
@@ -53,11 +54,17 @@ export async function useZenstackReadMany(model, opts = {}) {
|
|
|
53
54
|
include: opts.include,
|
|
54
55
|
where: toValue(opts.where),
|
|
55
56
|
orderBy: toValue(opts.orderBy),
|
|
56
|
-
skip:
|
|
57
|
-
take:
|
|
57
|
+
skip: skip.value,
|
|
58
|
+
take: opts.take
|
|
58
59
|
});
|
|
59
60
|
return { q: json };
|
|
60
61
|
}
|
|
62
|
+
async function fetchMore() {
|
|
63
|
+
if (canFetchMore.value) {
|
|
64
|
+
skip.value += opts.take;
|
|
65
|
+
await refetch();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
61
68
|
async function refetch() {
|
|
62
69
|
if (opts.fetchPolicy === "cache-only")
|
|
63
70
|
return;
|
|
@@ -84,5 +91,5 @@ export async function useZenstackReadMany(model, opts = {}) {
|
|
|
84
91
|
canFetchMore.value = false;
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
|
-
return { data, error, status, canFetchMore, refetch };
|
|
94
|
+
return { data, error, status, canFetchMore, refetch, fetchMore };
|
|
88
95
|
}
|
|
@@ -2,5 +2,5 @@ import type { $Zmodel } from '#build/types/nuxt-zenstack';
|
|
|
2
2
|
export declare function useZenstackRealtime(): {
|
|
3
3
|
subscribe: (models: $Zmodel[]) => void;
|
|
4
4
|
unsubscribe: (models: $Zmodel[]) => void;
|
|
5
|
-
getSubscriptions: () =>
|
|
5
|
+
getSubscriptions: () => string[];
|
|
6
6
|
};
|
|
@@ -1,46 +1,55 @@
|
|
|
1
1
|
import { useZenstackStore } from "../useZenstackStore/index.js";
|
|
2
|
-
import { useZenstackRead } from "../useZenstackRead/index.js";
|
|
3
2
|
import destr from "destr";
|
|
4
|
-
import { joinURL } from "ufo";
|
|
3
|
+
import { joinURL, withQuery } from "ufo";
|
|
5
4
|
import { getConfig } from "../common.js";
|
|
6
5
|
export function useZenstackRealtime() {
|
|
7
6
|
const config = getConfig();
|
|
7
|
+
const store = useZenstackStore();
|
|
8
8
|
const url = joinURL(config.baseUrl, config.apiPath, "realtime");
|
|
9
9
|
const subscriptions = /* @__PURE__ */ new Set();
|
|
10
10
|
let eventSource = null;
|
|
11
11
|
function connect() {
|
|
12
|
+
if (!import.meta.client)
|
|
13
|
+
return;
|
|
12
14
|
if (eventSource)
|
|
13
15
|
return;
|
|
14
|
-
|
|
16
|
+
const urlWithQuery = withQuery(url, { subscriptions: Array.from(subscriptions).join(",") });
|
|
17
|
+
eventSource = new EventSource(urlWithQuery);
|
|
15
18
|
eventSource.onmessage = (event) => {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
onMessage(
|
|
19
|
+
const message = destr(event.data);
|
|
20
|
+
if (message)
|
|
21
|
+
onMessage(message);
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
|
-
async function onMessage(
|
|
22
|
-
if (!subscriptions.has(
|
|
24
|
+
async function onMessage(message) {
|
|
25
|
+
if (!subscriptions.has(message.model))
|
|
23
26
|
return;
|
|
24
|
-
for (const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
27
|
+
for (const item of message.items) {
|
|
28
|
+
if (message.action === "delete")
|
|
29
|
+
store.deleteOne(message.model, item.id);
|
|
30
|
+
else
|
|
31
|
+
store.setOne(message.model, item);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
function disconnect() {
|
|
35
|
+
if (!import.meta.client)
|
|
36
|
+
return;
|
|
33
37
|
if (eventSource)
|
|
34
38
|
eventSource.close();
|
|
35
39
|
eventSource = null;
|
|
36
40
|
}
|
|
37
41
|
function subscribe(models) {
|
|
42
|
+
if (!import.meta.client)
|
|
43
|
+
return;
|
|
38
44
|
for (const model of models)
|
|
39
45
|
subscriptions.add(model);
|
|
46
|
+
disconnect();
|
|
40
47
|
if (subscriptions.size > 0)
|
|
41
48
|
connect();
|
|
42
49
|
}
|
|
43
50
|
function unsubscribe(models) {
|
|
51
|
+
if (!import.meta.client)
|
|
52
|
+
return;
|
|
44
53
|
for (const model of models)
|
|
45
54
|
subscriptions.delete(model);
|
|
46
55
|
if (subscriptions.size === 0)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { $Zmodel, $Zid } from '#build/types/nuxt-zenstack';
|
|
2
|
+
import { denormalize } from 'normalizr';
|
|
2
3
|
type FetchEntry = {
|
|
3
4
|
model: string;
|
|
4
5
|
method: 'GET' | 'PATCH' | 'POST' | 'DELETE';
|
|
@@ -12,13 +13,13 @@ export declare function useZenstackStore(): {
|
|
|
12
13
|
state: Readonly<import("vue").Ref<{
|
|
13
14
|
readonly [x: string]: {
|
|
14
15
|
readonly [x: string]: {
|
|
15
|
-
readonly [x: string]: string | number | boolean | readonly string[] | null;
|
|
16
|
+
readonly [x: string]: string | number | boolean | object | readonly (string | number)[] | null;
|
|
16
17
|
};
|
|
17
18
|
};
|
|
18
19
|
}, {
|
|
19
20
|
readonly [x: string]: {
|
|
20
21
|
readonly [x: string]: {
|
|
21
|
-
readonly [x: string]: string | number | boolean | readonly string[] | null;
|
|
22
|
+
readonly [x: string]: string | number | boolean | object | readonly (string | number)[] | null;
|
|
22
23
|
};
|
|
23
24
|
};
|
|
24
25
|
}>>;
|
|
@@ -42,7 +43,7 @@ export declare function useZenstackStore(): {
|
|
|
42
43
|
setOne: (model: $Zmodel, input: object) => void;
|
|
43
44
|
setMany: (model: $Zmodel, input: object[]) => void;
|
|
44
45
|
getOne: <Zmodel extends $Zmodel>(model: Zmodel, input: $Zid<Zmodel>) => any;
|
|
45
|
-
getMany: (model: $Zmodel) =>
|
|
46
|
+
getMany: (model: $Zmodel) => ReturnType<typeof denormalize>;
|
|
46
47
|
deleteOne: <Model extends $Zmodel>(model: Model, id: $Zid<Model>) => void;
|
|
47
48
|
deleteMany: (model: $Zmodel) => void;
|
|
48
49
|
addToFetchHistory: (entry: Omit<FetchEntry, "ssr" | "timestamp">) => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateNormalizrSchema, getNormalizrSchema, mergeNormalizedData } from "./normalization.js";
|
|
1
|
+
import { generateNormalizrSchema, getNormalizrSchema, linkNormalizedRelations, mergeNormalizedData, breakCircularReferences } from "./normalization.js";
|
|
2
2
|
import { normalize, denormalize } from "normalizr";
|
|
3
3
|
import { readonly, useState } from "#imports";
|
|
4
4
|
generateNormalizrSchema();
|
|
@@ -8,21 +8,25 @@ export function useZenstackStore() {
|
|
|
8
8
|
function setOne(model, input) {
|
|
9
9
|
const schema = getNormalizrSchema(model);
|
|
10
10
|
const res = normalize(input, schema);
|
|
11
|
-
state.value = mergeNormalizedData(res.entities, state.value);
|
|
11
|
+
state.value = linkNormalizedRelations(mergeNormalizedData(res.entities, state.value));
|
|
12
12
|
}
|
|
13
13
|
function setMany(model, input) {
|
|
14
14
|
const schema = [getNormalizrSchema(model)];
|
|
15
15
|
const res = normalize(input, schema);
|
|
16
|
-
state.value = mergeNormalizedData(res.entities, state.value);
|
|
16
|
+
state.value = linkNormalizedRelations(mergeNormalizedData(res.entities, state.value));
|
|
17
17
|
}
|
|
18
18
|
function getOne(model, input) {
|
|
19
19
|
const schema = getNormalizrSchema(model);
|
|
20
|
-
|
|
20
|
+
const denormalized = denormalize(input, schema, state.value);
|
|
21
|
+
if (!denormalized)
|
|
22
|
+
return null;
|
|
23
|
+
return breakCircularReferences(denormalized);
|
|
21
24
|
}
|
|
22
25
|
function getMany(model) {
|
|
23
26
|
const input = Object.keys(state.value[model.toString()] ?? {});
|
|
24
27
|
const schema = [getNormalizrSchema(model)];
|
|
25
|
-
|
|
28
|
+
const denormalized = denormalize(input, schema, state.value) ?? [];
|
|
29
|
+
return breakCircularReferences(denormalized);
|
|
26
30
|
}
|
|
27
31
|
function deleteOne(model, id) {
|
|
28
32
|
const newState = {};
|
|
@@ -52,8 +56,8 @@ export function useZenstackStore() {
|
|
|
52
56
|
body: entry.body,
|
|
53
57
|
id: entry.id,
|
|
54
58
|
query: entry.query,
|
|
55
|
-
ssr: typeof window === "undefined",
|
|
56
|
-
timestamp:
|
|
59
|
+
ssr: typeof globalThis.window === "undefined",
|
|
60
|
+
timestamp: Date.now()
|
|
57
61
|
});
|
|
58
62
|
}
|
|
59
63
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { schema as normalizrSchema } from 'normalizr';
|
|
2
2
|
import type { $Zmodel } from '#build/types/nuxt-zenstack';
|
|
3
|
-
type
|
|
3
|
+
type NormalizedId = string | number;
|
|
4
|
+
type NormalizedDataField = number | string | boolean | null | object | NormalizedId[];
|
|
4
5
|
/**
|
|
5
6
|
* Normalized data structure
|
|
6
7
|
* {
|
|
@@ -13,6 +14,34 @@ type NormalizedDataField = number | string | boolean | null | string[];
|
|
|
13
14
|
*/
|
|
14
15
|
export type NormalizedData = Record<string, Record<string, Record<string, NormalizedDataField>>>;
|
|
15
16
|
export declare function getNormalizrSchema(model: $Zmodel): normalizrSchema.Entity<any>;
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Breaks circular references in a denormalized object by replacing circular references with IDs.
|
|
19
|
+
* This prevents "Converting circular structure to JSON" errors.
|
|
20
|
+
*
|
|
21
|
+
* @param obj - The denormalized object that may contain circular references
|
|
22
|
+
* @param visited - Set of visited object references (used internally for recursion)
|
|
23
|
+
* @returns A new object with circular references replaced by IDs
|
|
24
|
+
*/
|
|
25
|
+
export declare function breakCircularReferences(obj: unknown, visited?: WeakSet<object>): unknown;
|
|
26
|
+
export declare function generateNormalizrSchema(): Map<string, normalizrSchema.Entity<any>>;
|
|
17
27
|
export declare function mergeNormalizedData(newData: NormalizedData, currentData: NormalizedData): NormalizedData;
|
|
28
|
+
/**
|
|
29
|
+
* Ensures relations are linked bidirectionally in normalized state.
|
|
30
|
+
*
|
|
31
|
+
* Problem: Normalizr only updates relations that are present in the payload.
|
|
32
|
+
* When a payload contains only foreign keys (e.g., `authorId`) but not the
|
|
33
|
+
* relational field (e.g., `author`) or the opposite side (e.g., `User.posts`),
|
|
34
|
+
* relations remain unlinked.
|
|
35
|
+
*
|
|
36
|
+
* Solution: This function uses ZenStack schema metadata to automatically link
|
|
37
|
+
* both sides of relations based on foreign keys.
|
|
38
|
+
*
|
|
39
|
+
* Example:
|
|
40
|
+
* - User created without posts
|
|
41
|
+
* - Post created with only authorId="user123"
|
|
42
|
+
* - This function will:
|
|
43
|
+
* 1. Set Post.author = "user123" (from authorId)
|
|
44
|
+
* 2. Add "post456" to User.posts array
|
|
45
|
+
*/
|
|
46
|
+
export declare function linkNormalizedRelations(input: NormalizedData): NormalizedData;
|
|
18
47
|
export {};
|
|
@@ -8,6 +8,29 @@ export function getNormalizrSchema(model) {
|
|
|
8
8
|
throw new Error(`Model ${model.toString()} not defined`);
|
|
9
9
|
return schema;
|
|
10
10
|
}
|
|
11
|
+
export function breakCircularReferences(obj, visited = /* @__PURE__ */ new WeakSet()) {
|
|
12
|
+
if (obj === null || typeof obj !== "object") {
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(obj)) {
|
|
16
|
+
return obj.map((item) => breakCircularReferences(item, visited));
|
|
17
|
+
}
|
|
18
|
+
if (visited.has(obj)) {
|
|
19
|
+
if (obj && typeof obj === "object" && "id" in obj) {
|
|
20
|
+
return { id: obj.id };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
visited.add(obj);
|
|
25
|
+
try {
|
|
26
|
+
const result = {};
|
|
27
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
28
|
+
result[key] = breakCircularReferences(value, visited);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
} finally {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
11
34
|
export function generateNormalizrSchema() {
|
|
12
35
|
const models = getModels();
|
|
13
36
|
for (const model of models) {
|
|
@@ -28,17 +51,141 @@ export function generateNormalizrSchema() {
|
|
|
28
51
|
entity.define(definition);
|
|
29
52
|
}
|
|
30
53
|
}
|
|
54
|
+
return NORMALIZR_SCHEMA;
|
|
31
55
|
}
|
|
32
56
|
export function mergeNormalizedData(newData, currentData) {
|
|
33
57
|
const merged = defu(newData, currentData);
|
|
34
58
|
for (const model in merged) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
const modelEntities = merged[model];
|
|
60
|
+
if (!modelEntities)
|
|
61
|
+
continue;
|
|
62
|
+
for (const id in modelEntities) {
|
|
63
|
+
const entity = modelEntities[id];
|
|
64
|
+
if (!entity)
|
|
65
|
+
continue;
|
|
66
|
+
for (const field in entity) {
|
|
67
|
+
const value = entity[field];
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
entity[field] = Array.from(new Set(value));
|
|
70
|
+
} else if (typeof value === "object" && typeof newData[model]?.[id]?.[field] !== "undefined") {
|
|
71
|
+
entity[field] = newData[model]?.[id]?.[field];
|
|
39
72
|
}
|
|
40
73
|
}
|
|
41
74
|
}
|
|
42
75
|
}
|
|
43
76
|
return merged;
|
|
44
77
|
}
|
|
78
|
+
function isIdValue(val) {
|
|
79
|
+
return typeof val === "string" || typeof val === "number";
|
|
80
|
+
}
|
|
81
|
+
function cloneNormalizedData(input) {
|
|
82
|
+
const cloned = {};
|
|
83
|
+
for (const modelName in input) {
|
|
84
|
+
const sourceModel = input[modelName];
|
|
85
|
+
if (!sourceModel)
|
|
86
|
+
continue;
|
|
87
|
+
cloned[modelName] = {};
|
|
88
|
+
const destModel = cloned[modelName];
|
|
89
|
+
if (!destModel)
|
|
90
|
+
continue;
|
|
91
|
+
for (const id in sourceModel) {
|
|
92
|
+
const entity = sourceModel[id];
|
|
93
|
+
if (entity)
|
|
94
|
+
destModel[id] = { ...entity };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return cloned;
|
|
98
|
+
}
|
|
99
|
+
function extractRelatedIds(entity, relationFieldName, isArrayRelation, foreignKeyFieldName) {
|
|
100
|
+
const relationValue = entity[relationFieldName];
|
|
101
|
+
if (isArrayRelation) {
|
|
102
|
+
if (Array.isArray(relationValue)) {
|
|
103
|
+
return relationValue.filter(isIdValue);
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
} else {
|
|
107
|
+
if (isIdValue(relationValue)) {
|
|
108
|
+
return [relationValue];
|
|
109
|
+
}
|
|
110
|
+
if (foreignKeyFieldName) {
|
|
111
|
+
const fkValue = entity[foreignKeyFieldName];
|
|
112
|
+
if (isIdValue(fkValue)) {
|
|
113
|
+
;
|
|
114
|
+
entity[relationFieldName] = fkValue;
|
|
115
|
+
return [fkValue];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function updateOppositeRelation(relatedEntity, oppositeFieldName, isArrayRelation, sourceEntityId) {
|
|
122
|
+
const entity = relatedEntity;
|
|
123
|
+
if (isArrayRelation) {
|
|
124
|
+
const currentValue = relatedEntity[oppositeFieldName];
|
|
125
|
+
const existingIds = Array.isArray(currentValue) ? currentValue.filter(isIdValue) : [];
|
|
126
|
+
if (!existingIds.includes(sourceEntityId)) {
|
|
127
|
+
entity[oppositeFieldName] = [...existingIds, sourceEntityId];
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
const currentValue = relatedEntity[oppositeFieldName];
|
|
131
|
+
if (!isIdValue(currentValue)) {
|
|
132
|
+
entity[oppositeFieldName] = sourceEntityId;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function linkNormalizedRelations(input) {
|
|
137
|
+
const linked = cloneNormalizedData(input);
|
|
138
|
+
const allModels = getModels();
|
|
139
|
+
for (const model of allModels) {
|
|
140
|
+
const modelName = model.toString();
|
|
141
|
+
const modelEntities = linked[modelName];
|
|
142
|
+
if (!modelEntities)
|
|
143
|
+
continue;
|
|
144
|
+
const modelDefinition = getModelDef(model);
|
|
145
|
+
for (const field of Object.values(modelDefinition.fields)) {
|
|
146
|
+
if (!isValidModel(field.type))
|
|
147
|
+
continue;
|
|
148
|
+
if (!field.relation?.opposite)
|
|
149
|
+
continue;
|
|
150
|
+
const relatedModel = field.type;
|
|
151
|
+
const relatedModelName = relatedModel.toString();
|
|
152
|
+
const relationFieldName = field.name;
|
|
153
|
+
const oppositeFieldName = field.relation.opposite;
|
|
154
|
+
const isArrayRelation = field.array ?? false;
|
|
155
|
+
const foreignKeyFields = field.relation.fields ?? [];
|
|
156
|
+
const foreignKeyFieldName = foreignKeyFields.length === 1 ? foreignKeyFields[0] : void 0;
|
|
157
|
+
const relatedModelDefinition = getModelDef(relatedModel);
|
|
158
|
+
const oppositeFieldDefinition = relatedModelDefinition.fields[oppositeFieldName];
|
|
159
|
+
if (!oppositeFieldDefinition)
|
|
160
|
+
continue;
|
|
161
|
+
const isOppositeArray = oppositeFieldDefinition.array ?? false;
|
|
162
|
+
for (const [entityId, entity] of Object.entries(modelEntities)) {
|
|
163
|
+
if (!entity)
|
|
164
|
+
continue;
|
|
165
|
+
const relatedIds = extractRelatedIds(
|
|
166
|
+
entity,
|
|
167
|
+
relationFieldName,
|
|
168
|
+
isArrayRelation,
|
|
169
|
+
foreignKeyFieldName
|
|
170
|
+
);
|
|
171
|
+
if (relatedIds.length === 0)
|
|
172
|
+
continue;
|
|
173
|
+
const relatedModelEntities = linked[relatedModelName];
|
|
174
|
+
if (!relatedModelEntities)
|
|
175
|
+
continue;
|
|
176
|
+
for (const relatedId of relatedIds) {
|
|
177
|
+
const relatedEntity = relatedModelEntities[String(relatedId)];
|
|
178
|
+
if (!relatedEntity)
|
|
179
|
+
continue;
|
|
180
|
+
updateOppositeRelation(
|
|
181
|
+
relatedEntity,
|
|
182
|
+
oppositeFieldName,
|
|
183
|
+
isOppositeArray,
|
|
184
|
+
entityId
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return linked;
|
|
191
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
2
|
-
data:
|
|
3
|
-
[x: string]: any;
|
|
4
|
-
} & {};
|
|
2
|
+
data: import("../../../../../../.nuxt/types/nuxt-zenstack.js").$Zitem<string, import("../../../../../../.nuxt/types/nuxt-zenstack.js").$Zinclude<string>>;
|
|
5
3
|
}>>;
|
|
6
4
|
export default _default;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
2
|
-
data: (
|
|
3
|
-
[x: string]: any;
|
|
4
|
-
} & {})[];
|
|
2
|
+
data: import("../../../../../../.nuxt/types/nuxt-zenstack.js").$Zitem<string, import("../../../../../../.nuxt/types/nuxt-zenstack.js").$Zinclude<string>>[];
|
|
5
3
|
}>>;
|
|
6
4
|
export default _default;
|
|
@@ -1,19 +1,38 @@
|
|
|
1
|
-
import { defineEventHandler, createEventStream } from "h3";
|
|
1
|
+
import { defineEventHandler, createEventStream, getQuery } from "h3";
|
|
2
2
|
import { useNitroApp } from "nitropack/runtime";
|
|
3
3
|
import { getConfig } from "../utils/helpers.js";
|
|
4
|
+
import { useZenstack } from "../utils/index.js";
|
|
4
5
|
export default defineEventHandler(async (event) => {
|
|
5
6
|
const eventStream = createEventStream(event);
|
|
6
7
|
const nitroApp = useNitroApp();
|
|
7
8
|
const config = getConfig();
|
|
8
|
-
|
|
9
|
+
const zenstack = useZenstack();
|
|
10
|
+
const query = getQuery(event);
|
|
11
|
+
const subscriptions = query.subscriptions?.split(",") ?? [];
|
|
12
|
+
nitroApp.hooks.hook("zenstack:publish", async ({ action, model, items }) => {
|
|
13
|
+
if (!subscriptions.includes(model)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
9
16
|
if (!config.expose[model]?.find((permission) => permission === "read")) {
|
|
10
17
|
return;
|
|
11
18
|
}
|
|
12
19
|
const message = {
|
|
13
20
|
model,
|
|
14
21
|
action,
|
|
15
|
-
|
|
22
|
+
items: []
|
|
16
23
|
};
|
|
24
|
+
if (action === "delete") {
|
|
25
|
+
message.items = items.map((item) => ({ id: item.id }));
|
|
26
|
+
} else {
|
|
27
|
+
const allowedItemsPromises = items.map((item) => zenstack.findUnique({
|
|
28
|
+
client: event.context.zenstack.client,
|
|
29
|
+
model,
|
|
30
|
+
// @ts-expect-error id not known here
|
|
31
|
+
id: item.id
|
|
32
|
+
}));
|
|
33
|
+
const allowedItems = await Promise.all(allowedItemsPromises);
|
|
34
|
+
message.items = allowedItems.map((item) => item.data);
|
|
35
|
+
}
|
|
17
36
|
eventStream.push(JSON.stringify(message));
|
|
18
37
|
});
|
|
19
38
|
eventStream.onClosed(async () => {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { $Zmodel, $Zdef, $ZcreateData, $ZupdateData } from '#build/types/nuxt-zenstack';
|
|
2
|
-
import type { PrivateModuleOptions } from '../../types/index.js';
|
|
3
2
|
export declare function getModelDef(model: $Zmodel): $Zdef;
|
|
4
3
|
export declare function isIdString(model: $Zmodel): boolean;
|
|
5
4
|
export declare function getModels(): $Zmodel[];
|
|
6
5
|
export declare function isValidModel(model: $Zmodel): boolean;
|
|
7
6
|
export declare function sanitizeCreateData<Zmodel extends $Zmodel>(model: Zmodel, data: $ZcreateData<Zmodel>): $ZcreateData<Zmodel>;
|
|
8
7
|
export declare function sanitizeUpdateData<Zmodel extends $Zmodel>(model: Zmodel, data: $ZupdateData<Zmodel>): $ZupdateData<Zmodel>;
|
|
9
|
-
export declare function getConfig():
|
|
8
|
+
export declare function getConfig(): {
|
|
9
|
+
expose: Partial<Record<$Zmodel, Array<"create" | "read" | "update" | "delete">>>;
|
|
10
|
+
apiPath: string;
|
|
11
|
+
fetchPolicy: import("../../types/index.js").FetchPolicy;
|
|
12
|
+
realtime: boolean;
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
};
|
|
@@ -3,7 +3,7 @@ import { update } from './operations/update.js';
|
|
|
3
3
|
import { findUnique } from './operations/findUnique.js';
|
|
4
4
|
import { findMany } from './operations/findMany.js';
|
|
5
5
|
import { create } from './operations/create.js';
|
|
6
|
-
import type { $Zclient,
|
|
6
|
+
import type { $Zclient, RealtimeData } from '#build/types/nuxt-zenstack';
|
|
7
7
|
import type { H3Event } from 'h3';
|
|
8
8
|
export declare function useZenstack(): {
|
|
9
9
|
delete: typeof _delete;
|
|
@@ -12,5 +12,5 @@ export declare function useZenstack(): {
|
|
|
12
12
|
findMany: typeof findMany;
|
|
13
13
|
create: typeof create;
|
|
14
14
|
setSessionClient: (event: H3Event, client: $Zclient) => void;
|
|
15
|
-
publish: (
|
|
15
|
+
publish: (args: RealtimeData) => Promise<any>;
|
|
16
16
|
};
|
|
@@ -11,9 +11,9 @@ export function useZenstack() {
|
|
|
11
11
|
else
|
|
12
12
|
event.context.zenstack = { client };
|
|
13
13
|
}
|
|
14
|
-
function publish(
|
|
14
|
+
function publish(args) {
|
|
15
15
|
const nitroApp = useNitroApp();
|
|
16
|
-
return nitroApp.hooks.callHook("zenstack:publish",
|
|
16
|
+
return nitroApp.hooks.callHook("zenstack:publish", args);
|
|
17
17
|
}
|
|
18
18
|
return { delete: _delete, update, findUnique, findMany, create, setSessionClient, publish };
|
|
19
19
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getModelOperations, createError } from "./common.js";
|
|
2
|
-
import { sanitizeCreateData } from "../helpers.js";
|
|
2
|
+
import { sanitizeCreateData, getConfig } from "../helpers.js";
|
|
3
3
|
import { useZenstack } from "../index.js";
|
|
4
4
|
export async function create(args) {
|
|
5
5
|
const operations = getModelOperations(args.client, args.model);
|
|
@@ -9,10 +9,11 @@ export async function create(args) {
|
|
|
9
9
|
}).catch((err) => {
|
|
10
10
|
throw createError(err);
|
|
11
11
|
});
|
|
12
|
-
|
|
12
|
+
const config = getConfig();
|
|
13
|
+
args.publish ??= config.realtime;
|
|
13
14
|
if (args.publish) {
|
|
14
15
|
const zenstack = useZenstack();
|
|
15
|
-
await zenstack.publish("create", args.model, [data]);
|
|
16
|
+
await zenstack.publish({ action: "create", model: args.model, items: [data] });
|
|
16
17
|
}
|
|
17
18
|
return { data };
|
|
18
19
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useZenstack } from "../index.js";
|
|
2
2
|
import { getModelOperations, createError } from "./common.js";
|
|
3
|
+
import { getConfig } from "../helpers.js";
|
|
3
4
|
export async function _delete(args) {
|
|
4
5
|
const operations = getModelOperations(args.client, args.model);
|
|
5
6
|
const data = await operations.delete({
|
|
@@ -10,10 +11,11 @@ export async function _delete(args) {
|
|
|
10
11
|
}).catch((err) => {
|
|
11
12
|
throw createError(err);
|
|
12
13
|
});
|
|
13
|
-
|
|
14
|
+
const config = getConfig();
|
|
15
|
+
args.publish ??= config.realtime;
|
|
14
16
|
if (args.publish) {
|
|
15
17
|
const zenstack = useZenstack();
|
|
16
|
-
await zenstack.publish("delete", args.model, [data]);
|
|
18
|
+
await zenstack.publish({ action: "delete", model: args.model, items: [data] });
|
|
17
19
|
}
|
|
18
20
|
return { data };
|
|
19
21
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getModelOperations, createError } from "./common.js";
|
|
2
|
-
import { sanitizeUpdateData } from "../helpers.js";
|
|
2
|
+
import { sanitizeUpdateData, getConfig } from "../helpers.js";
|
|
3
3
|
import { useZenstack } from "../index.js";
|
|
4
4
|
export async function update(args) {
|
|
5
5
|
const operations = getModelOperations(args.client, args.model);
|
|
@@ -13,10 +13,11 @@ export async function update(args) {
|
|
|
13
13
|
}).catch((err) => {
|
|
14
14
|
throw createError(err);
|
|
15
15
|
});
|
|
16
|
-
|
|
16
|
+
const config = getConfig();
|
|
17
|
+
args.publish ??= config.realtime;
|
|
17
18
|
if (args.publish) {
|
|
18
19
|
const zenstack = useZenstack();
|
|
19
|
-
await zenstack.publish("update", args.model, [data]);
|
|
20
|
+
await zenstack.publish({ action: "update", model: args.model, items: [data] });
|
|
20
21
|
}
|
|
21
22
|
return { data };
|
|
22
23
|
}
|
|
@@ -8,7 +8,7 @@ type ReadArgs<Zmodel extends $Zmodel> = {
|
|
|
8
8
|
take?: number;
|
|
9
9
|
};
|
|
10
10
|
export declare function parseClient(event: H3Event): import("@zenstackhq/orm").ClientContract<SchemaType>;
|
|
11
|
-
export declare function parseModel(event: H3Event): string
|
|
11
|
+
export declare function parseModel(event: H3Event): string;
|
|
12
12
|
export declare function parseId(event: H3Event, model: $Zmodel): string | number;
|
|
13
13
|
export declare function parseReadArgs<Zmodel extends $Zmodel>(event: H3Event, _model: Zmodel): ReadArgs<Zmodel>;
|
|
14
14
|
export declare function parseCreateArgs<Zmodel extends $Zmodel>(event: H3Event, _model: Zmodel): Promise<{
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import type { ClientContract, ModelOperations, ModelResult, IncludeInput, WhereInput, FindManyArgs,
|
|
1
|
+
import type { ModelResult, ClientContract, ModelOperations, ModelResult, IncludeInput, WhereInput, FindManyArgs, CreateArgs, UpdateArgs, ORMError, RejectedByPolicyReason, ORMErrorReason } from '@zenstackhq/orm'
|
|
2
2
|
import type { SchemaType } from '~~/zenstack/schema'
|
|
3
|
-
import type { ModelDef } from '@zenstackhq/orm/schema'
|
|
3
|
+
import type { ModelDef, GetModels } from '@zenstackhq/orm/schema'
|
|
4
4
|
import type { H3Error } from 'h3'
|
|
5
5
|
|
|
6
|
-
type ItemGetPayload<Zmodel extends $Zmodel, Args extends SelectIncludeOmit<SchemaType, Zmodel, true>, Options extends QueryOptions<SchemaType> = QueryOptions<SchemaType>> = SimplifiedPlainResult<SchemaType, Zmodel, Args, Options>
|
|
7
|
-
|
|
8
6
|
export type $Zschema = SchemaType
|
|
9
7
|
export type $Zclient = ClientContract<SchemaType>
|
|
10
|
-
export type $Zmodel =
|
|
8
|
+
export type $Zmodel = GetModels<SchemaType>
|
|
11
9
|
export type $Zdef = ModelDef
|
|
12
10
|
export type $Zoperations<Zmodel extends $Zmodel> = ModelOperations<SchemaType, Zmodel>
|
|
13
11
|
export type $Zid<Zmodel extends $Zmodel> = ModelResult<SchemaType, Zmodel> extends { id: infer Id } ? Id : never
|
|
14
12
|
export type $Zinclude<Zmodel extends $Zmodel> = IncludeInput<SchemaType, Zmodel> | undefined
|
|
15
|
-
export type $Zitem<Zmodel extends $Zmodel, Zinclude extends $Zinclude = undefined> =
|
|
13
|
+
export type $Zitem<Zmodel extends $Zmodel, Zinclude extends $Zinclude = undefined> = ModelResult<SchemaType, Zmodel, { include: Zinclude }>
|
|
16
14
|
export type $Zwhere<Zmodel extends $Zmodel> = WhereInput<SchemaType, Zmodel> | undefined
|
|
17
15
|
export type $ZorderBy<Zmodel extends $Zmodel> = FindManyArgs<SchemaType, Zmodel>['orderBy'] | undefined
|
|
18
16
|
|
|
@@ -39,14 +37,14 @@ declare module 'h3' {
|
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
interface RealtimeData<Zmodel extends $Zmodel = $Zmodel> {
|
|
43
41
|
action: 'create' | 'update' | 'delete'
|
|
44
|
-
model:
|
|
45
|
-
|
|
42
|
+
model: Zmodel
|
|
43
|
+
items: $Zitem<Zmodel>[]
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
declare module 'nitropack' {
|
|
49
47
|
interface NitroRuntimeHooks {
|
|
50
|
-
'zenstack:publish': (
|
|
48
|
+
'zenstack:publish': (args: RealtimeData) => Promise<void> | void
|
|
51
49
|
}
|
|
52
50
|
}
|