@happyvertical/smrt-tags 0.37.1 → 0.37.3
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/index.js +668 -735
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/smrt-knowledge.json +7 -7
- package/dist/types.js +0 -2
- package/dist/utils.js +39 -42
- package/dist/utils.js.map +1 -1
- package/package.json +11 -11
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,747 +1,680 @@
|
|
|
1
|
-
import { ObjectRegistry, field, smrt, SmrtHierarchical, SmrtObject, SmrtCollection } from "@happyvertical/smrt-core";
|
|
2
|
-
import { tenantId, TenantScoped, queryGlobal, queryWithGlobals } from "@happyvertical/smrt-tenancy";
|
|
3
1
|
import { calculateLevel, generateUniqueSlug, hasCircularReference, sanitizeSlug, validateSlug } from "./utils.js";
|
|
4
|
-
ObjectRegistry
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { ObjectRegistry, SmrtCollection, SmrtHierarchical, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
|
|
3
|
+
import { TenantScoped, queryGlobal, queryWithGlobals, tenantId } from "@happyvertical/smrt-tenancy";
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
var __defProp$2 = Object.defineProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) __defProp$2(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
if (!no_symbols) __defProp$2(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/__smrt-register__.ts
|
|
17
|
+
ObjectRegistry.registerPackageManifest(new URL("./manifest.json", "" + import.meta.url));
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/tag.ts
|
|
7
20
|
var __defProp$1 = Object.defineProperty;
|
|
8
21
|
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
9
22
|
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (kind && result) __defProp$1(target, key, result);
|
|
15
|
-
return result;
|
|
23
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
24
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
25
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
26
|
+
return result;
|
|
16
27
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// slug-friendly methods (moveTag / mergeTag / getChildren etc.) so the
|
|
104
|
-
// existing public API surface keeps working for callers that prefer
|
|
105
|
-
// slug references.
|
|
106
|
-
/**
|
|
107
|
-
* Convenience method for slug-based lookup
|
|
108
|
-
*
|
|
109
|
-
* @param slug - The slug to search for
|
|
110
|
-
* @param context - Optional context filter
|
|
111
|
-
* @returns Tag instance or null if not found
|
|
112
|
-
*/
|
|
113
|
-
static async getBySlug(_slug, _context) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Get root tags (no parent) for a context
|
|
118
|
-
*
|
|
119
|
-
* @param context - The context to filter by
|
|
120
|
-
* @returns Array of root tags
|
|
121
|
-
*/
|
|
122
|
-
static async getRootTags(_context = "global") {
|
|
123
|
-
return [];
|
|
124
|
-
}
|
|
28
|
+
var Tag = class extends SmrtHierarchical {
|
|
29
|
+
_slug = "";
|
|
30
|
+
_context = "global";
|
|
31
|
+
get slug() {
|
|
32
|
+
return this._slug;
|
|
33
|
+
}
|
|
34
|
+
set slug(value) {
|
|
35
|
+
this._slug = value;
|
|
36
|
+
}
|
|
37
|
+
get context() {
|
|
38
|
+
return this._context;
|
|
39
|
+
}
|
|
40
|
+
set context(value) {
|
|
41
|
+
this._context = value;
|
|
42
|
+
}
|
|
43
|
+
name = "";
|
|
44
|
+
level = 0;
|
|
45
|
+
description = "";
|
|
46
|
+
metadata = "";
|
|
47
|
+
tenantId = null;
|
|
48
|
+
createdAt = /* @__PURE__ */ new Date();
|
|
49
|
+
updatedAt = /* @__PURE__ */ new Date();
|
|
50
|
+
constructor(options = {}) {
|
|
51
|
+
super(options);
|
|
52
|
+
if (options.name) this.name = options.name;
|
|
53
|
+
if (options.parentId !== void 0) this.parentId = options.parentId ?? null;
|
|
54
|
+
if (options.slug) this._slug = options.slug;
|
|
55
|
+
if (options.context !== void 0) this._context = options.context;
|
|
56
|
+
if (options.level !== void 0) this.level = options.level;
|
|
57
|
+
if (options.description !== void 0) this.description = options.description;
|
|
58
|
+
if (options.metadata !== void 0) if (typeof options.metadata === "string") this.metadata = options.metadata;
|
|
59
|
+
else this.metadata = JSON.stringify(options.metadata);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get metadata as parsed object
|
|
63
|
+
*
|
|
64
|
+
* @returns Parsed metadata object or empty object if no metadata
|
|
65
|
+
*/
|
|
66
|
+
getMetadata() {
|
|
67
|
+
const metadataValue = String(this.metadata || "");
|
|
68
|
+
if (!metadataValue) return {};
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(metadataValue);
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Set metadata from object
|
|
77
|
+
*
|
|
78
|
+
* @param data - Metadata object to store
|
|
79
|
+
*/
|
|
80
|
+
setMetadata(data) {
|
|
81
|
+
this.metadata = JSON.stringify(data);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Update metadata by merging with existing values
|
|
85
|
+
*
|
|
86
|
+
* @param updates - Partial metadata to merge
|
|
87
|
+
*/
|
|
88
|
+
updateMetadata(updates) {
|
|
89
|
+
const current = this.getMetadata();
|
|
90
|
+
this.setMetadata({
|
|
91
|
+
...current,
|
|
92
|
+
...updates
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Convenience method for slug-based lookup
|
|
97
|
+
*
|
|
98
|
+
* @param slug - The slug to search for
|
|
99
|
+
* @param context - Optional context filter
|
|
100
|
+
* @returns Tag instance or null if not found
|
|
101
|
+
*/
|
|
102
|
+
static async getBySlug(_slug, _context) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get root tags (no parent) for a context
|
|
107
|
+
*
|
|
108
|
+
* @param context - The context to filter by
|
|
109
|
+
* @returns Array of root tags
|
|
110
|
+
*/
|
|
111
|
+
static async getRootTags(_context = "global") {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
125
114
|
};
|
|
126
|
-
__decorateClass$1([
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
115
|
+
__decorateClass$1([field({ required: true })], Tag.prototype, "name", 2);
|
|
116
|
+
__decorateClass$1([tenantId({ nullable: true })], Tag.prototype, "tenantId", 2);
|
|
117
|
+
Tag = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
|
|
118
|
+
tableStrategy: "sti",
|
|
119
|
+
api: { include: [
|
|
120
|
+
"list",
|
|
121
|
+
"get",
|
|
122
|
+
"create",
|
|
123
|
+
"update",
|
|
124
|
+
"delete"
|
|
125
|
+
] },
|
|
126
|
+
mcp: { include: [
|
|
127
|
+
"list",
|
|
128
|
+
"get",
|
|
129
|
+
"create",
|
|
130
|
+
"update"
|
|
131
|
+
] },
|
|
132
|
+
cli: true
|
|
133
|
+
})], Tag);
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/tag-alias.ts
|
|
141
136
|
var __defProp = Object.defineProperty;
|
|
142
137
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
143
138
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
140
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
141
|
+
if (kind && result) __defProp(target, key, result);
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
var TagAlias = class extends SmrtObject {
|
|
145
|
+
tagSlug = "";
|
|
146
|
+
alias = "";
|
|
147
|
+
language = "";
|
|
148
|
+
_context = "";
|
|
149
|
+
get context() {
|
|
150
|
+
return this._context;
|
|
151
|
+
}
|
|
152
|
+
set context(value) {
|
|
153
|
+
this._context = value;
|
|
154
|
+
}
|
|
155
|
+
tenantId = null;
|
|
156
|
+
createdAt = /* @__PURE__ */ new Date();
|
|
157
|
+
constructor(options = {}) {
|
|
158
|
+
super(options);
|
|
159
|
+
if (options.tagSlug !== void 0) this.tagSlug = options.tagSlug;
|
|
160
|
+
if (options.alias) this.alias = options.alias;
|
|
161
|
+
if (options.language !== void 0) this.language = options.language;
|
|
162
|
+
if (options.context !== void 0) this._context = options.context;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get the tag this alias belongs to
|
|
166
|
+
*
|
|
167
|
+
* @returns Tag instance or null if not found
|
|
168
|
+
*/
|
|
169
|
+
async getTag() {
|
|
170
|
+
const { TagCollection } = await Promise.resolve().then(() => tags_exports);
|
|
171
|
+
return await (await TagCollection.create(this.options)).get({ slug: this.tagSlug });
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Search tags by alias
|
|
175
|
+
*
|
|
176
|
+
* @param alias - The alias to search for
|
|
177
|
+
* @param language - Optional language filter
|
|
178
|
+
* @returns Array of matching tags
|
|
179
|
+
*/
|
|
180
|
+
static async searchByAlias(_alias, _language) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get all aliases for a tag
|
|
185
|
+
*
|
|
186
|
+
* @param tagSlug - The tag slug to get aliases for
|
|
187
|
+
* @returns Array of TagAlias instances
|
|
188
|
+
*/
|
|
189
|
+
static async getAliasesForTag(_tagSlug) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
150
192
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
__decorateClass([field({ required: true })], TagAlias.prototype, "alias", 2);
|
|
194
|
+
__decorateClass([tenantId({ nullable: true })], TagAlias.prototype, "tenantId", 2);
|
|
195
|
+
TagAlias = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
|
|
196
|
+
tableStrategy: "sti",
|
|
197
|
+
api: { include: [
|
|
198
|
+
"list",
|
|
199
|
+
"get",
|
|
200
|
+
"create",
|
|
201
|
+
"update",
|
|
202
|
+
"delete"
|
|
203
|
+
] },
|
|
204
|
+
mcp: { include: [
|
|
205
|
+
"list",
|
|
206
|
+
"get",
|
|
207
|
+
"create"
|
|
208
|
+
] },
|
|
209
|
+
cli: true
|
|
210
|
+
})], TagAlias);
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/tag-aliases.ts
|
|
213
|
+
var tag_aliases_exports = /* @__PURE__ */ __exportAll({ TagAliasCollection: () => TagAliasCollection });
|
|
214
|
+
var TagAliasCollection = class extends SmrtCollection {
|
|
215
|
+
static _itemClass = TagAlias;
|
|
216
|
+
/**
|
|
217
|
+
* Add an alias to a tag (get or create)
|
|
218
|
+
*
|
|
219
|
+
* @param tagSlug - The tag slug
|
|
220
|
+
* @param alias - The alias text
|
|
221
|
+
* @param language - Optional language code
|
|
222
|
+
* @param context - Optional context
|
|
223
|
+
* @returns TagAlias instance
|
|
224
|
+
*/
|
|
225
|
+
async addAlias(tagSlug, alias, language, context) {
|
|
226
|
+
const where = {
|
|
227
|
+
tagSlug,
|
|
228
|
+
alias
|
|
229
|
+
};
|
|
230
|
+
if (language) where.language = language;
|
|
231
|
+
if (context) where.context = context;
|
|
232
|
+
const existing = await this.list({
|
|
233
|
+
where,
|
|
234
|
+
limit: 1
|
|
235
|
+
});
|
|
236
|
+
if (existing.length > 0) return existing[0];
|
|
237
|
+
return await this.create({
|
|
238
|
+
tagSlug,
|
|
239
|
+
alias,
|
|
240
|
+
language,
|
|
241
|
+
context
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Search tags by alias
|
|
246
|
+
*
|
|
247
|
+
* @param alias - The alias to search for
|
|
248
|
+
* @param language - Optional language filter
|
|
249
|
+
* @returns Array of matching tags
|
|
250
|
+
*/
|
|
251
|
+
async searchByAlias(alias, language) {
|
|
252
|
+
const where = { alias };
|
|
253
|
+
if (language) where.language = language;
|
|
254
|
+
const aliases = await this.list({ where });
|
|
255
|
+
const tagSlugs = [...new Set(aliases.map((a) => a.tagSlug))];
|
|
256
|
+
const { TagCollection } = await Promise.resolve().then(() => tags_exports);
|
|
257
|
+
const tagCollection = await TagCollection.create(this.options);
|
|
258
|
+
const tags = [];
|
|
259
|
+
for (const slug of tagSlugs) {
|
|
260
|
+
const tag = await tagCollection.get({ slug });
|
|
261
|
+
if (tag) tags.push(tag);
|
|
262
|
+
}
|
|
263
|
+
return tags;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get all aliases for a tag
|
|
267
|
+
*
|
|
268
|
+
* @param tagSlug - The tag slug
|
|
269
|
+
* @param language - Optional language filter
|
|
270
|
+
* @returns Array of TagAlias instances
|
|
271
|
+
*/
|
|
272
|
+
async getAliasesForTag(tagSlug, language) {
|
|
273
|
+
const where = { tagSlug };
|
|
274
|
+
if (language) where.language = language;
|
|
275
|
+
return await this.list({ where });
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Remove an alias by ID
|
|
279
|
+
*
|
|
280
|
+
* @param aliasId - The alias UUID
|
|
281
|
+
*/
|
|
282
|
+
async removeAlias(aliasId) {
|
|
283
|
+
const alias = await this.get({ id: aliasId });
|
|
284
|
+
if (alias) await alias.delete();
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Bulk add aliases to a tag
|
|
288
|
+
*
|
|
289
|
+
* @param tagSlug - The tag slug
|
|
290
|
+
* @param aliases - Array of alias configurations
|
|
291
|
+
* @returns Array of created TagAlias instances
|
|
292
|
+
*/
|
|
293
|
+
async bulkAddAliases(tagSlug, aliases) {
|
|
294
|
+
const created = [];
|
|
295
|
+
for (const aliasData of aliases) {
|
|
296
|
+
const tagAlias = await this.addAlias(tagSlug, aliasData.alias, aliasData.language, aliasData.context);
|
|
297
|
+
created.push(tagAlias);
|
|
298
|
+
}
|
|
299
|
+
return created;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get aliases grouped by language
|
|
303
|
+
*
|
|
304
|
+
* @param tagSlug - The tag slug
|
|
305
|
+
* @returns Map of language code to array of aliases
|
|
306
|
+
*/
|
|
307
|
+
async getAliasesByLanguage(tagSlug) {
|
|
308
|
+
const aliases = await this.getAliasesForTag(tagSlug);
|
|
309
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
310
|
+
for (const alias of aliases) {
|
|
311
|
+
const lang = String(alias.language || "default");
|
|
312
|
+
if (!grouped.has(lang)) grouped.set(lang, []);
|
|
313
|
+
grouped.get(lang)?.push(String(alias.alias));
|
|
314
|
+
}
|
|
315
|
+
return grouped;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Find matching aliases (case-insensitive partial match)
|
|
319
|
+
*
|
|
320
|
+
* Note: This is a simple implementation. For production use,
|
|
321
|
+
* consider using full-text search or fuzzy matching.
|
|
322
|
+
*
|
|
323
|
+
* @param query - The search query
|
|
324
|
+
* @param language - Optional language filter
|
|
325
|
+
* @returns Array of matching TagAlias instances
|
|
326
|
+
*/
|
|
327
|
+
async findMatchingAliases(query, language) {
|
|
328
|
+
const where = {};
|
|
329
|
+
if (language) where.language = language;
|
|
330
|
+
const all = await this.list({ where });
|
|
331
|
+
const queryLower = query.toLowerCase();
|
|
332
|
+
return all.filter((alias) => String(alias.alias).toLowerCase().includes(queryLower));
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Find all tag aliases belonging to a specific tenant
|
|
336
|
+
*
|
|
337
|
+
* @param tenantId - The tenant ID to filter by
|
|
338
|
+
* @returns Array of tag aliases for the specified tenant
|
|
339
|
+
*/
|
|
340
|
+
async findByTenant(tenantId) {
|
|
341
|
+
return this.list({ where: { tenantId } });
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Find all global (tenant-less) tag aliases.
|
|
345
|
+
*
|
|
346
|
+
* Routes through the shared tenant-global helper so it does not throw under
|
|
347
|
+
* an active tenant context (an explicit `tenant_id IS NULL` filter would be
|
|
348
|
+
* flagged as an isolation violation). (#1600)
|
|
349
|
+
*
|
|
350
|
+
* @returns Array of global tag aliases with null tenantId
|
|
351
|
+
*/
|
|
352
|
+
async findGlobal() {
|
|
353
|
+
return queryGlobal(this);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Find tag aliases for a tenant including global aliases.
|
|
357
|
+
*
|
|
358
|
+
* Fails closed if an active tenant context requests a different tenant's
|
|
359
|
+
* rows; the admin/system path keeps the cross-tenant capability. (#1600)
|
|
360
|
+
*
|
|
361
|
+
* @param tenantId - The tenant ID to filter by
|
|
362
|
+
* @returns Array of tag aliases for the tenant plus all global aliases
|
|
363
|
+
*/
|
|
364
|
+
async findWithGlobals(tenantId) {
|
|
365
|
+
return queryWithGlobals(this, tenantId, "TagAlias.findWithGlobals");
|
|
366
|
+
}
|
|
206
367
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
* Delegating to `moveTo` would write `parentId` first and `level` in a
|
|
516
|
-
* second save — if the second save failed, the tag would be left with
|
|
517
|
-
* the new parent but a stale level, breaking the depth cache.
|
|
518
|
-
*
|
|
519
|
-
* After the moved tag persists, descendant levels are recalculated
|
|
520
|
-
* recursively via `updateDescendantLevels`.
|
|
521
|
-
*
|
|
522
|
-
* @param slug - The tag to move
|
|
523
|
-
* @param newParentSlug - The new parent slug (null for root)
|
|
524
|
-
* @param context - Optional context. When provided, both source and new
|
|
525
|
-
* parent are resolved within it. When omitted, both slugs must be
|
|
526
|
-
* unambiguous across contexts; the resolver throws otherwise.
|
|
527
|
-
* @throws Error if either slug fails to resolve, if either slug is
|
|
528
|
-
* ambiguous across contexts (no context provided), if source and new
|
|
529
|
-
* parent live in different contexts, or if the move would create a
|
|
530
|
-
* cycle.
|
|
531
|
-
*/
|
|
532
|
-
async moveTag(slug, newParentSlug, context) {
|
|
533
|
-
const tag = await this.resolveBySlug(slug, context);
|
|
534
|
-
if (!tag) throw new Error(`Tag '${slug}' not found`);
|
|
535
|
-
let newParent = null;
|
|
536
|
-
if (newParentSlug) {
|
|
537
|
-
newParent = await this.resolveBySlug(
|
|
538
|
-
newParentSlug,
|
|
539
|
-
context ?? tag.context
|
|
540
|
-
);
|
|
541
|
-
if (!newParent) {
|
|
542
|
-
throw new Error(`Tag '${newParentSlug}' not found`);
|
|
543
|
-
}
|
|
544
|
-
if (newParent.context !== tag.context) {
|
|
545
|
-
throw new Error(
|
|
546
|
-
`Cannot move Tag '${slug}' (context '${tag.context}') under '${newParentSlug}' (context '${newParent.context}') — contexts must match.`
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
const newParentId = newParent?.id ?? null;
|
|
551
|
-
if (newParentId !== null && newParentId === tag.id) {
|
|
552
|
-
throw new Error(`Cannot move Tag ${tag.id} to itself.`);
|
|
553
|
-
}
|
|
554
|
-
if (newParentId !== null) {
|
|
555
|
-
const descendants = await tag.getDescendants();
|
|
556
|
-
if (descendants.some((d) => d.id === newParentId)) {
|
|
557
|
-
throw new Error(
|
|
558
|
-
`Cannot move Tag ${tag.id} under one of its own descendants (${newParentId}) — would create a cycle.`
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
tag.parentId = newParentId;
|
|
563
|
-
tag.level = newParent ? newParent.level + 1 : 0;
|
|
564
|
-
await tag.save();
|
|
565
|
-
await this.updateDescendantLevels(tag);
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Merge one tag into another (updates all references)
|
|
569
|
-
*
|
|
570
|
-
* Reparents `fromTag`'s direct children onto `toTag` and recalculates
|
|
571
|
-
* their `level` field plus the level of every descendant — without
|
|
572
|
-
* this, children moved from a different depth would carry stale
|
|
573
|
-
* levels relative to their new parent. `TagAlias.tagSlug` references
|
|
574
|
-
* are also rewritten, then `fromTag` is deleted.
|
|
575
|
-
*
|
|
576
|
-
* Note: Consuming packages are responsible for updating their own
|
|
577
|
-
* join tables (e.g. `asset_tags`).
|
|
578
|
-
*
|
|
579
|
-
* @param fromSlug - The tag to merge from
|
|
580
|
-
* @param toSlug - The tag to merge into
|
|
581
|
-
* @param context - Optional context. When provided, both tags are
|
|
582
|
-
* resolved within it. When omitted, both slugs must be unambiguous
|
|
583
|
-
* across contexts.
|
|
584
|
-
* @throws Error if either slug fails to resolve, if either slug is
|
|
585
|
-
* ambiguous, or if the two tags live in different contexts.
|
|
586
|
-
*/
|
|
587
|
-
async mergeTag(fromSlug, toSlug, context) {
|
|
588
|
-
const fromTag = await this.resolveBySlug(fromSlug, context);
|
|
589
|
-
const toTag = await this.resolveBySlug(toSlug, context ?? fromTag?.context);
|
|
590
|
-
if (!fromTag) throw new Error(`Source tag '${fromSlug}' not found`);
|
|
591
|
-
if (!toTag) throw new Error(`Target tag '${toSlug}' not found`);
|
|
592
|
-
if (!fromTag.id) throw new Error(`Source tag '${fromSlug}' has no id`);
|
|
593
|
-
if (!toTag.id) throw new Error(`Target tag '${toSlug}' has no id`);
|
|
594
|
-
if (fromTag.context !== toTag.context) {
|
|
595
|
-
throw new Error(
|
|
596
|
-
`Cannot merge '${fromSlug}' (context '${fromTag.context}') into '${toSlug}' (context '${toTag.context}') — contexts must match.`
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
if (fromTag.id === toTag.id) {
|
|
600
|
-
throw new Error(`Cannot merge tag '${fromSlug}' into itself.`);
|
|
601
|
-
}
|
|
602
|
-
const fromDescendants = await fromTag.getDescendants();
|
|
603
|
-
if (fromDescendants.some((d) => d.id === toTag.id)) {
|
|
604
|
-
throw new Error(
|
|
605
|
-
`Cannot merge '${fromSlug}' into '${toSlug}' — target is a descendant of source (would create a cycle).`
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
const children = await this.list({
|
|
609
|
-
where: { parentId: fromTag.id }
|
|
610
|
-
});
|
|
611
|
-
const newChildLevel = toTag.level + 1;
|
|
612
|
-
for (const child of children) {
|
|
613
|
-
child.parentId = toTag.id;
|
|
614
|
-
child.level = newChildLevel;
|
|
615
|
-
await child.save();
|
|
616
|
-
await this.updateDescendantLevels(child);
|
|
617
|
-
}
|
|
618
|
-
const { TagAliasCollection: TagAliasCollection2 } = await Promise.resolve().then(() => tagAliases);
|
|
619
|
-
const aliasCollection = await TagAliasCollection2.create(this.options);
|
|
620
|
-
const aliasContexts = [fromTag.context];
|
|
621
|
-
if (fromTag.context === "global") {
|
|
622
|
-
aliasContexts.push("");
|
|
623
|
-
}
|
|
624
|
-
const aliases = await aliasCollection.list({
|
|
625
|
-
where: { tagSlug: fromSlug, context: aliasContexts }
|
|
626
|
-
});
|
|
627
|
-
for (const alias of aliases) {
|
|
628
|
-
alias.tagSlug = toSlug;
|
|
629
|
-
await alias.save();
|
|
630
|
-
}
|
|
631
|
-
await fromTag.delete();
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Remove tags with no references (cleanup unused tags)
|
|
635
|
-
*
|
|
636
|
-
* Note: This requires consuming packages to provide usage information.
|
|
637
|
-
* By default, only removes tags with no children and no aliases.
|
|
638
|
-
*
|
|
639
|
-
* @param context - Optional context to filter cleanup
|
|
640
|
-
*/
|
|
641
|
-
async cleanupUnused(context) {
|
|
642
|
-
const where = {};
|
|
643
|
-
if (context) where.context = context;
|
|
644
|
-
const tags2 = await this.list({ where });
|
|
645
|
-
const { TagAliasCollection: TagAliasCollection2 } = await Promise.resolve().then(() => tagAliases);
|
|
646
|
-
const aliasCollection = await TagAliasCollection2.create(this.options);
|
|
647
|
-
let deletedCount = 0;
|
|
648
|
-
for (const tag of tags2) {
|
|
649
|
-
if (!tag.id) continue;
|
|
650
|
-
const children = await this.list({
|
|
651
|
-
where: { parentId: tag.id },
|
|
652
|
-
limit: 1
|
|
653
|
-
});
|
|
654
|
-
if (children.length > 0) continue;
|
|
655
|
-
const aliases = await aliasCollection.list({
|
|
656
|
-
where: { tagSlug: tag.slug },
|
|
657
|
-
limit: 1
|
|
658
|
-
});
|
|
659
|
-
if (aliases.length > 0) continue;
|
|
660
|
-
await tag.delete();
|
|
661
|
-
deletedCount++;
|
|
662
|
-
}
|
|
663
|
-
return deletedCount;
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Calculate hierarchy level for a tag, looking the parent up by slug.
|
|
667
|
-
*
|
|
668
|
-
* @param parentSlug - The parent tag slug (null/empty for root)
|
|
669
|
-
* @param context - Optional context for the parent lookup. When
|
|
670
|
-
* omitted, the parent slug must be unambiguous across contexts.
|
|
671
|
-
* @returns The calculated level (root parent → 1, missing parent → 0)
|
|
672
|
-
*/
|
|
673
|
-
async calculateLevel(parentSlug, context) {
|
|
674
|
-
if (!parentSlug) return 0;
|
|
675
|
-
const parent = await this.resolveBySlug(parentSlug, context);
|
|
676
|
-
if (!parent) return 0;
|
|
677
|
-
return parent.level + 1;
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Update levels for all descendants after moving a tag
|
|
681
|
-
*
|
|
682
|
-
* @param tag - The tag that was moved
|
|
683
|
-
*/
|
|
684
|
-
async updateDescendantLevels(tag) {
|
|
685
|
-
if (!tag.id) return;
|
|
686
|
-
const children = await this.list({
|
|
687
|
-
where: { parentId: tag.id }
|
|
688
|
-
});
|
|
689
|
-
for (const child of children) {
|
|
690
|
-
child.level = tag.level + 1;
|
|
691
|
-
await child.save();
|
|
692
|
-
await this.updateDescendantLevels(child);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
// =========================================================================
|
|
696
|
-
// Tenant Helper Methods
|
|
697
|
-
// =========================================================================
|
|
698
|
-
/**
|
|
699
|
-
* Find all tags belonging to a specific tenant
|
|
700
|
-
*
|
|
701
|
-
* @param tenantId - The tenant ID to filter by
|
|
702
|
-
* @returns Array of tags for the specified tenant
|
|
703
|
-
*/
|
|
704
|
-
async findByTenant(tenantId2) {
|
|
705
|
-
return this.list({ where: { tenantId: tenantId2 } });
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Find all global (tenant-less) tags.
|
|
709
|
-
*
|
|
710
|
-
* Routes through the shared tenant-global helper so it does not throw under
|
|
711
|
-
* an active tenant context (an explicit `tenant_id IS NULL` filter would be
|
|
712
|
-
* flagged as an isolation violation). (#1600)
|
|
713
|
-
*
|
|
714
|
-
* @returns Array of global tags with null tenantId
|
|
715
|
-
*/
|
|
716
|
-
async findGlobal() {
|
|
717
|
-
return queryGlobal(this);
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Find tags for a tenant including global tags.
|
|
721
|
-
*
|
|
722
|
-
* Fails closed if an active tenant context requests a different tenant's
|
|
723
|
-
* rows; the admin/system path keeps the cross-tenant capability. (#1600)
|
|
724
|
-
*
|
|
725
|
-
* @param tenantId - The tenant ID to filter by
|
|
726
|
-
* @returns Array of tags for the tenant plus all global tags
|
|
727
|
-
*/
|
|
728
|
-
async findWithGlobals(tenantId2) {
|
|
729
|
-
return queryWithGlobals(this, tenantId2, "Tag.findWithGlobals");
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
const tags = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
733
|
-
__proto__: null,
|
|
734
|
-
TagCollection
|
|
735
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
736
|
-
export {
|
|
737
|
-
Tag,
|
|
738
|
-
TagAlias,
|
|
739
|
-
TagAliasCollection,
|
|
740
|
-
TagCollection,
|
|
741
|
-
calculateLevel,
|
|
742
|
-
generateUniqueSlug,
|
|
743
|
-
hasCircularReference,
|
|
744
|
-
sanitizeSlug,
|
|
745
|
-
validateSlug
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/tags.ts
|
|
370
|
+
var tags_exports = /* @__PURE__ */ __exportAll({ TagCollection: () => TagCollection });
|
|
371
|
+
var TagCollection = class extends SmrtCollection {
|
|
372
|
+
static _itemClass = Tag;
|
|
373
|
+
/**
|
|
374
|
+
* Get or create a tag with context
|
|
375
|
+
*
|
|
376
|
+
* @param slug - Tag slug
|
|
377
|
+
* @param context - Tag context (default: 'global')
|
|
378
|
+
* @returns Tag instance
|
|
379
|
+
*/
|
|
380
|
+
async getOrCreate(slug, context = "global") {
|
|
381
|
+
const existing = await this.list({
|
|
382
|
+
where: {
|
|
383
|
+
slug,
|
|
384
|
+
context
|
|
385
|
+
},
|
|
386
|
+
limit: 1
|
|
387
|
+
});
|
|
388
|
+
if (existing.length > 0) return existing[0];
|
|
389
|
+
return await this.create({
|
|
390
|
+
slug,
|
|
391
|
+
name: slug.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
392
|
+
context,
|
|
393
|
+
level: 0
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Resolve a tag by slug, optionally scoped to a context.
|
|
398
|
+
*
|
|
399
|
+
* Tags are identified by `(slug, context)`. When `context` is omitted
|
|
400
|
+
* and the slug exists in more than one context, this throws a clear
|
|
401
|
+
* ambiguity error rather than silently picking the first matching row.
|
|
402
|
+
* Callers that know their context should pass it; callers that work in
|
|
403
|
+
* a single-context world can leave it off.
|
|
404
|
+
*
|
|
405
|
+
* @returns The matching Tag, or `null` if nothing matches.
|
|
406
|
+
* @throws Error if `context` is omitted and the slug is ambiguous.
|
|
407
|
+
*/
|
|
408
|
+
async resolveBySlug(slug, context) {
|
|
409
|
+
const where = { slug };
|
|
410
|
+
if (context !== void 0) where.context = context;
|
|
411
|
+
const matches = await this.list({
|
|
412
|
+
where,
|
|
413
|
+
limit: 2
|
|
414
|
+
});
|
|
415
|
+
if (matches.length === 0) return null;
|
|
416
|
+
if (matches.length > 1) throw new Error(`Tag slug '${slug}' is ambiguous: resolves to ${matches.length}+ rows across contexts. Pass an explicit \`context\` argument.`);
|
|
417
|
+
return matches[0];
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* List tags by context with optional parent filtering by slug.
|
|
421
|
+
*
|
|
422
|
+
* @param context - The context to filter by
|
|
423
|
+
* @param parentSlug - Optional parent slug to filter children. Pass an
|
|
424
|
+
* empty string or `null` to find root tags; pass a slug to find that
|
|
425
|
+
* tag's immediate children. Typed as `string | null` so TypeScript
|
|
426
|
+
* callers can pass `null` without a cast — the `null` and `''` paths
|
|
427
|
+
* are both treated as "roots only".
|
|
428
|
+
* @returns Array of matching tags
|
|
429
|
+
*/
|
|
430
|
+
async listByContext(context, parentSlug) {
|
|
431
|
+
const where = { context };
|
|
432
|
+
if (parentSlug === "" || parentSlug === null) where.parentId = null;
|
|
433
|
+
else if (parentSlug !== void 0) {
|
|
434
|
+
const parent = await this.get({
|
|
435
|
+
slug: parentSlug,
|
|
436
|
+
context
|
|
437
|
+
});
|
|
438
|
+
if (!parent?.id) return [];
|
|
439
|
+
where.parentId = parent.id;
|
|
440
|
+
}
|
|
441
|
+
return await this.list({ where });
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Get root tags (no parent) for a context
|
|
445
|
+
*
|
|
446
|
+
* @param context - The context to filter by (default: 'global')
|
|
447
|
+
* @returns Array of root tags
|
|
448
|
+
*/
|
|
449
|
+
async getRootTags(context = "global") {
|
|
450
|
+
return await this.list({ where: {
|
|
451
|
+
context,
|
|
452
|
+
parentId: null
|
|
453
|
+
} });
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get immediate children of a parent tag, looked up by slug.
|
|
457
|
+
*
|
|
458
|
+
* @param parentSlug - The parent tag slug
|
|
459
|
+
* @param context - Optional context for the parent lookup. When omitted,
|
|
460
|
+
* the parent slug must be unambiguous across contexts (throws if not).
|
|
461
|
+
* @returns Array of child tags, or `[]` if the parent slug doesn't
|
|
462
|
+
* resolve. Children are filtered to the resolved parent's context so
|
|
463
|
+
* cross-context children don't leak in.
|
|
464
|
+
*/
|
|
465
|
+
async getChildren(parentSlug, context) {
|
|
466
|
+
const parent = await this.resolveBySlug(parentSlug, context);
|
|
467
|
+
if (!parent?.id) return [];
|
|
468
|
+
return await this.list({ where: {
|
|
469
|
+
parentId: parent.id,
|
|
470
|
+
context: parent.context
|
|
471
|
+
} });
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get tag hierarchy (all ancestors and descendants)
|
|
475
|
+
*
|
|
476
|
+
* @param slug - The tag slug
|
|
477
|
+
* @param context - Optional context for the slug lookup. When omitted,
|
|
478
|
+
* the slug must be unambiguous across contexts.
|
|
479
|
+
* @returns Object with ancestors, current tag, and descendants
|
|
480
|
+
*/
|
|
481
|
+
async getHierarchy(slug, context) {
|
|
482
|
+
const tag = await this.resolveBySlug(slug, context);
|
|
483
|
+
if (!tag) throw new Error(`Tag '${slug}' not found`);
|
|
484
|
+
const [ancestors, descendants] = await Promise.all([tag.getAncestors(), tag.getDescendants()]);
|
|
485
|
+
return {
|
|
486
|
+
ancestors,
|
|
487
|
+
current: tag,
|
|
488
|
+
descendants
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Move a tag to a new parent. Slug-based API; UUIDs resolved internally.
|
|
493
|
+
*
|
|
494
|
+
* Cycle detection is inlined here (mirroring `SmrtHierarchical.moveTo`'s
|
|
495
|
+
* self-loop + descendant checks) so that both `parentId` and the
|
|
496
|
+
* denormalised `level` field can be persisted in a single `save()`.
|
|
497
|
+
* Delegating to `moveTo` would write `parentId` first and `level` in a
|
|
498
|
+
* second save — if the second save failed, the tag would be left with
|
|
499
|
+
* the new parent but a stale level, breaking the depth cache.
|
|
500
|
+
*
|
|
501
|
+
* After the moved tag persists, descendant levels are recalculated
|
|
502
|
+
* recursively via `updateDescendantLevels`.
|
|
503
|
+
*
|
|
504
|
+
* @param slug - The tag to move
|
|
505
|
+
* @param newParentSlug - The new parent slug (null for root)
|
|
506
|
+
* @param context - Optional context. When provided, both source and new
|
|
507
|
+
* parent are resolved within it. When omitted, both slugs must be
|
|
508
|
+
* unambiguous across contexts; the resolver throws otherwise.
|
|
509
|
+
* @throws Error if either slug fails to resolve, if either slug is
|
|
510
|
+
* ambiguous across contexts (no context provided), if source and new
|
|
511
|
+
* parent live in different contexts, or if the move would create a
|
|
512
|
+
* cycle.
|
|
513
|
+
*/
|
|
514
|
+
async moveTag(slug, newParentSlug, context) {
|
|
515
|
+
const tag = await this.resolveBySlug(slug, context);
|
|
516
|
+
if (!tag) throw new Error(`Tag '${slug}' not found`);
|
|
517
|
+
let newParent = null;
|
|
518
|
+
if (newParentSlug) {
|
|
519
|
+
newParent = await this.resolveBySlug(newParentSlug, context ?? tag.context);
|
|
520
|
+
if (!newParent) throw new Error(`Tag '${newParentSlug}' not found`);
|
|
521
|
+
if (newParent.context !== tag.context) throw new Error(`Cannot move Tag '${slug}' (context '${tag.context}') under '${newParentSlug}' (context '${newParent.context}') \u2014 contexts must match.`);
|
|
522
|
+
}
|
|
523
|
+
const newParentId = newParent?.id ?? null;
|
|
524
|
+
if (newParentId !== null && newParentId === tag.id) throw new Error(`Cannot move Tag ${tag.id} to itself.`);
|
|
525
|
+
if (newParentId !== null) {
|
|
526
|
+
if ((await tag.getDescendants()).some((d) => d.id === newParentId)) throw new Error(`Cannot move Tag ${tag.id} under one of its own descendants (${newParentId}) \u2014 would create a cycle.`);
|
|
527
|
+
}
|
|
528
|
+
tag.parentId = newParentId;
|
|
529
|
+
tag.level = newParent ? newParent.level + 1 : 0;
|
|
530
|
+
await tag.save();
|
|
531
|
+
await this.updateDescendantLevels(tag);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Merge one tag into another (updates all references)
|
|
535
|
+
*
|
|
536
|
+
* Reparents `fromTag`'s direct children onto `toTag` and recalculates
|
|
537
|
+
* their `level` field plus the level of every descendant — without
|
|
538
|
+
* this, children moved from a different depth would carry stale
|
|
539
|
+
* levels relative to their new parent. `TagAlias.tagSlug` references
|
|
540
|
+
* are also rewritten, then `fromTag` is deleted.
|
|
541
|
+
*
|
|
542
|
+
* Note: Consuming packages are responsible for updating their own
|
|
543
|
+
* join tables (e.g. `asset_tags`).
|
|
544
|
+
*
|
|
545
|
+
* @param fromSlug - The tag to merge from
|
|
546
|
+
* @param toSlug - The tag to merge into
|
|
547
|
+
* @param context - Optional context. When provided, both tags are
|
|
548
|
+
* resolved within it. When omitted, both slugs must be unambiguous
|
|
549
|
+
* across contexts.
|
|
550
|
+
* @throws Error if either slug fails to resolve, if either slug is
|
|
551
|
+
* ambiguous, or if the two tags live in different contexts.
|
|
552
|
+
*/
|
|
553
|
+
async mergeTag(fromSlug, toSlug, context) {
|
|
554
|
+
const fromTag = await this.resolveBySlug(fromSlug, context);
|
|
555
|
+
const toTag = await this.resolveBySlug(toSlug, context ?? fromTag?.context);
|
|
556
|
+
if (!fromTag) throw new Error(`Source tag '${fromSlug}' not found`);
|
|
557
|
+
if (!toTag) throw new Error(`Target tag '${toSlug}' not found`);
|
|
558
|
+
if (!fromTag.id) throw new Error(`Source tag '${fromSlug}' has no id`);
|
|
559
|
+
if (!toTag.id) throw new Error(`Target tag '${toSlug}' has no id`);
|
|
560
|
+
if (fromTag.context !== toTag.context) throw new Error(`Cannot merge '${fromSlug}' (context '${fromTag.context}') into '${toSlug}' (context '${toTag.context}') \u2014 contexts must match.`);
|
|
561
|
+
if (fromTag.id === toTag.id) throw new Error(`Cannot merge tag '${fromSlug}' into itself.`);
|
|
562
|
+
if ((await fromTag.getDescendants()).some((d) => d.id === toTag.id)) throw new Error(`Cannot merge '${fromSlug}' into '${toSlug}' \u2014 target is a descendant of source (would create a cycle).`);
|
|
563
|
+
const children = await this.list({ where: { parentId: fromTag.id } });
|
|
564
|
+
const newChildLevel = toTag.level + 1;
|
|
565
|
+
for (const child of children) {
|
|
566
|
+
child.parentId = toTag.id;
|
|
567
|
+
child.level = newChildLevel;
|
|
568
|
+
await child.save();
|
|
569
|
+
await this.updateDescendantLevels(child);
|
|
570
|
+
}
|
|
571
|
+
const { TagAliasCollection } = await Promise.resolve().then(() => tag_aliases_exports);
|
|
572
|
+
const aliasCollection = await TagAliasCollection.create(this.options);
|
|
573
|
+
const aliasContexts = [fromTag.context];
|
|
574
|
+
if (fromTag.context === "global") aliasContexts.push("");
|
|
575
|
+
const aliases = await aliasCollection.list({ where: {
|
|
576
|
+
tagSlug: fromSlug,
|
|
577
|
+
context: aliasContexts
|
|
578
|
+
} });
|
|
579
|
+
for (const alias of aliases) {
|
|
580
|
+
alias.tagSlug = toSlug;
|
|
581
|
+
await alias.save();
|
|
582
|
+
}
|
|
583
|
+
await fromTag.delete();
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Remove tags with no references (cleanup unused tags)
|
|
587
|
+
*
|
|
588
|
+
* Note: This requires consuming packages to provide usage information.
|
|
589
|
+
* By default, only removes tags with no children and no aliases.
|
|
590
|
+
*
|
|
591
|
+
* @param context - Optional context to filter cleanup
|
|
592
|
+
*/
|
|
593
|
+
async cleanupUnused(context) {
|
|
594
|
+
const where = {};
|
|
595
|
+
if (context) where.context = context;
|
|
596
|
+
const tags = await this.list({ where });
|
|
597
|
+
const { TagAliasCollection } = await Promise.resolve().then(() => tag_aliases_exports);
|
|
598
|
+
const aliasCollection = await TagAliasCollection.create(this.options);
|
|
599
|
+
let deletedCount = 0;
|
|
600
|
+
for (const tag of tags) {
|
|
601
|
+
if (!tag.id) continue;
|
|
602
|
+
if ((await this.list({
|
|
603
|
+
where: { parentId: tag.id },
|
|
604
|
+
limit: 1
|
|
605
|
+
})).length > 0) continue;
|
|
606
|
+
if ((await aliasCollection.list({
|
|
607
|
+
where: { tagSlug: tag.slug },
|
|
608
|
+
limit: 1
|
|
609
|
+
})).length > 0) continue;
|
|
610
|
+
await tag.delete();
|
|
611
|
+
deletedCount++;
|
|
612
|
+
}
|
|
613
|
+
return deletedCount;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Calculate hierarchy level for a tag, looking the parent up by slug.
|
|
617
|
+
*
|
|
618
|
+
* @param parentSlug - The parent tag slug (null/empty for root)
|
|
619
|
+
* @param context - Optional context for the parent lookup. When
|
|
620
|
+
* omitted, the parent slug must be unambiguous across contexts.
|
|
621
|
+
* @returns The calculated level (root parent → 1, missing parent → 0)
|
|
622
|
+
*/
|
|
623
|
+
async calculateLevel(parentSlug, context) {
|
|
624
|
+
if (!parentSlug) return 0;
|
|
625
|
+
const parent = await this.resolveBySlug(parentSlug, context);
|
|
626
|
+
if (!parent) return 0;
|
|
627
|
+
return parent.level + 1;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Update levels for all descendants after moving a tag
|
|
631
|
+
*
|
|
632
|
+
* @param tag - The tag that was moved
|
|
633
|
+
*/
|
|
634
|
+
async updateDescendantLevels(tag) {
|
|
635
|
+
if (!tag.id) return;
|
|
636
|
+
const children = await this.list({ where: { parentId: tag.id } });
|
|
637
|
+
for (const child of children) {
|
|
638
|
+
child.level = tag.level + 1;
|
|
639
|
+
await child.save();
|
|
640
|
+
await this.updateDescendantLevels(child);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Find all tags belonging to a specific tenant
|
|
645
|
+
*
|
|
646
|
+
* @param tenantId - The tenant ID to filter by
|
|
647
|
+
* @returns Array of tags for the specified tenant
|
|
648
|
+
*/
|
|
649
|
+
async findByTenant(tenantId) {
|
|
650
|
+
return this.list({ where: { tenantId } });
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Find all global (tenant-less) tags.
|
|
654
|
+
*
|
|
655
|
+
* Routes through the shared tenant-global helper so it does not throw under
|
|
656
|
+
* an active tenant context (an explicit `tenant_id IS NULL` filter would be
|
|
657
|
+
* flagged as an isolation violation). (#1600)
|
|
658
|
+
*
|
|
659
|
+
* @returns Array of global tags with null tenantId
|
|
660
|
+
*/
|
|
661
|
+
async findGlobal() {
|
|
662
|
+
return queryGlobal(this);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Find tags for a tenant including global tags.
|
|
666
|
+
*
|
|
667
|
+
* Fails closed if an active tenant context requests a different tenant's
|
|
668
|
+
* rows; the admin/system path keeps the cross-tenant capability. (#1600)
|
|
669
|
+
*
|
|
670
|
+
* @param tenantId - The tenant ID to filter by
|
|
671
|
+
* @returns Array of tags for the tenant plus all global tags
|
|
672
|
+
*/
|
|
673
|
+
async findWithGlobals(tenantId) {
|
|
674
|
+
return queryWithGlobals(this, tenantId, "Tag.findWithGlobals");
|
|
675
|
+
}
|
|
746
676
|
};
|
|
747
|
-
//#
|
|
677
|
+
//#endregion
|
|
678
|
+
export { Tag, TagAlias, TagAliasCollection, TagCollection, calculateLevel, generateUniqueSlug, hasCircularReference, sanitizeSlug, validateSlug };
|
|
679
|
+
|
|
680
|
+
//# sourceMappingURL=index.js.map
|