@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.
Files changed (29) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +1 -1
  3. package/dist/runtime/composables/index.d.ts +1 -0
  4. package/dist/runtime/composables/index.js +1 -0
  5. package/dist/runtime/composables/useZenstackRead/index.d.ts +1 -1
  6. package/dist/runtime/composables/useZenstackReadMany/index.d.ts +3 -7
  7. package/dist/runtime/composables/useZenstackReadMany/index.js +12 -5
  8. package/dist/runtime/composables/useZenstackRealtime/index.d.ts +1 -1
  9. package/dist/runtime/composables/useZenstackRealtime/index.js +23 -14
  10. package/dist/runtime/composables/useZenstackStore/index.d.ts +4 -3
  11. package/dist/runtime/composables/useZenstackStore/index.js +11 -7
  12. package/dist/runtime/composables/useZenstackStore/normalization.d.ts +31 -2
  13. package/dist/runtime/composables/useZenstackStore/normalization.js +151 -4
  14. package/dist/runtime/server/api/models/[model]/[id].delete.d.ts +1 -3
  15. package/dist/runtime/server/api/models/[model]/[id].get.d.ts +1 -3
  16. package/dist/runtime/server/api/models/[model]/[id].patch.d.ts +1 -3
  17. package/dist/runtime/server/api/models/[model]/index.get.d.ts +1 -3
  18. package/dist/runtime/server/api/models/[model]/index.post.d.ts +1 -3
  19. package/dist/runtime/server/routes/realtime.js +22 -3
  20. package/dist/runtime/server/utils/helpers.d.ts +7 -2
  21. package/dist/runtime/server/utils/helpers.js +1 -1
  22. package/dist/runtime/server/utils/index.d.ts +2 -2
  23. package/dist/runtime/server/utils/index.js +2 -2
  24. package/dist/runtime/server/utils/operations/create.js +4 -3
  25. package/dist/runtime/server/utils/operations/delete.js +4 -2
  26. package/dist/runtime/server/utils/operations/update.js +4 -3
  27. package/dist/runtime/server/utils/parsers.d.ts +1 -1
  28. package/dist/runtime/types/template.d.ts +8 -10
  29. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bg-dev/nuxt-zenstack",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "configKey": "zenstack",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
package/dist/module.mjs CHANGED
@@ -3,7 +3,7 @@ import { joinURL } from 'ufo';
3
3
  import { defu } from 'defu';
4
4
 
5
5
  const name = "@bg-dev/nuxt-zenstack";
6
- const version = "0.0.7";
6
+ const version = "0.0.8";
7
7
 
8
8
  const module$1 = defineNuxtModule({
9
9
  meta: {
@@ -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 srore when the query is resolved.
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 srore when the query is resolved.
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?: MaybeRefOrGetter<number>;
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, opts.skip, opts.take].filter((option) => isRef(option) || typeof option === "function");
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: toValue(opts.skip),
57
- take: toValue(opts.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: () => (string | number | symbol)[];
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
- eventSource = new EventSource(url);
16
+ const urlWithQuery = withQuery(url, { subscriptions: Array.from(subscriptions).join(",") });
17
+ eventSource = new EventSource(urlWithQuery);
15
18
  eventSource.onmessage = (event) => {
16
- const data = destr(event.data);
17
- if (data)
18
- onMessage(data);
19
+ const message = destr(event.data);
20
+ if (message)
21
+ onMessage(message);
19
22
  };
20
23
  }
21
- async function onMessage(data) {
22
- if (!subscriptions.has(data.model))
24
+ async function onMessage(message) {
25
+ if (!subscriptions.has(message.model))
23
26
  return;
24
- for (const id of data.ids) {
25
- if (data.action === "delete") {
26
- useZenstackStore().deleteOne(data.model, id);
27
- } else {
28
- await useZenstackRead(data.model, id, { fetchPolicy: "fetch-only" });
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) => any;
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
- return denormalize(input, schema, state.value) ?? null;
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
- return denormalize(input, schema, state.value) ?? [];
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: (/* @__PURE__ */ new Date()).getTime()
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 NormalizedDataField = number | string | boolean | null | string[];
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
- export declare function generateNormalizrSchema(): void;
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
- for (const id in merged[model]) {
36
- for (const field in merged[model][id]) {
37
- if (Array.isArray(merged[model][id][field])) {
38
- merged[model][id][field] = Array.from(new Set(merged[model][id][field]));
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>;
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,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>;
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,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>;
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
- nitroApp.hooks.hook("zenstack:publish", (action, model, items) => {
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
- ids: items.map((item) => item.id)
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(): PrivateModuleOptions;
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
+ };
@@ -35,5 +35,5 @@ export function sanitizeUpdateData(model, data) {
35
35
  }
36
36
  export function getConfig() {
37
37
  const config = useRuntimeConfig();
38
- return config.zenstack;
38
+ return { ...config.public.zenstack, ...config.zenstack };
39
39
  }
@@ -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, $Zmodel } from '#build/types/nuxt-zenstack';
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: (action: "create" | "update" | "delete", model: $Zmodel, items: Record<string, unknown>[]) => Promise<any>;
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(action, model, items) {
14
+ function publish(args) {
15
15
  const nitroApp = useNitroApp();
16
- return nitroApp.hooks.callHook("zenstack:publish", action, model, items);
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
- args.publish ??= true;
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
- args.publish ??= true;
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
- args.publish ??= true;
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 | number | symbol;
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, SimplifiedPlainResult, SelectIncludeOmit, QueryOptions, CreateArgs, UpdateArgs, ORMError, RejectedByPolicyReason, ORMErrorReason } from '@zenstackhq/orm'
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 = keyof SchemaType['models']
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> = ItemGetPayload<Zmodel, { include: Zinclude }>
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
- type RealtimeData = {
40
+ interface RealtimeData<Zmodel extends $Zmodel = $Zmodel> {
43
41
  action: 'create' | 'update' | 'delete'
44
- model: $Zmodel
45
- ids: Array<number | string>
42
+ model: Zmodel
43
+ items: $Zitem<Zmodel>[]
46
44
  }
47
45
 
48
46
  declare module 'nitropack' {
49
47
  interface NitroRuntimeHooks {
50
- 'zenstack:publish': (action: 'create' | 'update' | 'delete', model: $Zmodel, items: Record<string, unknown>[]) => Promise<void> | void
48
+ 'zenstack:publish': (args: RealtimeData) => Promise<void> | void
51
49
  }
52
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bg-dev/nuxt-zenstack",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "ZenStack integration for Nuxt",
5
5
  "repository": {
6
6
  "url": "https://github.com/becem-gharbi/nuxt-zenstack"