convex-cms 0.0.2 → 0.0.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/admin-dist/nitro.json +15 -0
- package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
- package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
- package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
- package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
- package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
- package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
- package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
- package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
- package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
- package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
- package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
- package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
- package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
- package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
- package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
- package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
- package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
- package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
- package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
- package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
- package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
- package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
- package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
- package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
- package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
- package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
- package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
- package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
- package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
- package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
- package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
- package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
- package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
- package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
- package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
- package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
- package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
- package/admin-dist/public/favicon.ico +0 -0
- package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
- package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
- package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
- package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
- package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
- package/admin-dist/server/_libs/clsx.mjs +16 -0
- package/admin-dist/server/_libs/cmdk.mjs +315 -0
- package/admin-dist/server/_libs/convex.mjs +4841 -0
- package/admin-dist/server/_libs/cookie-es.mjs +58 -0
- package/admin-dist/server/_libs/croner.mjs +1 -0
- package/admin-dist/server/_libs/crossws.mjs +1 -0
- package/admin-dist/server/_libs/date-fns.mjs +1716 -0
- package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
- package/admin-dist/server/_libs/get-nonce.mjs +9 -0
- package/admin-dist/server/_libs/h3-v2.mjs +277 -0
- package/admin-dist/server/_libs/h3.mjs +401 -0
- package/admin-dist/server/_libs/hookable.mjs +1 -0
- package/admin-dist/server/_libs/isbot.mjs +20 -0
- package/admin-dist/server/_libs/lucide-react.mjs +850 -0
- package/admin-dist/server/_libs/ohash.mjs +1 -0
- package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
- package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
- package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
- package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
- package/admin-dist/server/_libs/rou3.mjs +8 -0
- package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
- package/admin-dist/server/_libs/seroval.mjs +1765 -0
- package/admin-dist/server/_libs/srvx.mjs +719 -0
- package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
- package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
- package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
- package/admin-dist/server/_libs/tslib.mjs +39 -0
- package/admin-dist/server/_libs/ufo.mjs +54 -0
- package/admin-dist/server/_libs/unctx.mjs +1 -0
- package/admin-dist/server/_libs/unstorage.mjs +1 -0
- package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
- package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
- package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
- package/admin-dist/server/_libs/zod.mjs +4223 -0
- package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
- package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
- package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
- package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
- package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
- package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
- package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
- package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
- package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
- package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
- package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
- package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
- package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
- package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
- package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
- package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
- package/admin-dist/server/_ssr/index.mjs +1275 -0
- package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
- package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
- package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
- package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
- package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
- package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
- package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
- package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
- package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
- package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
- package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
- package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
- package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
- package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
- package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
- package/admin-dist/server/favicon.ico +0 -0
- package/admin-dist/server/index.mjs +627 -0
- package/dist/cli/index.js +0 -0
- package/dist/client/admin-config.d.ts +0 -1
- package/dist/client/admin-config.d.ts.map +1 -1
- package/dist/client/admin-config.js +0 -1
- package/dist/client/admin-config.js.map +1 -1
- package/dist/client/adminApi.d.ts.map +1 -1
- package/dist/client/agentTools.d.ts +1237 -135
- package/dist/client/agentTools.d.ts.map +1 -1
- package/dist/client/agentTools.js +33 -9
- package/dist/client/agentTools.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +9 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/mediaAssets.d.ts +35 -0
- package/dist/component/mediaAssets.d.ts.map +1 -1
- package/dist/component/mediaAssets.js +81 -0
- package/dist/component/mediaAssets.js.map +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +2 -1
- package/dist/test.js.map +1 -1
- package/package.json +9 -5
- package/dist/component/auditLog.d.ts +0 -410
- package/dist/component/auditLog.d.ts.map +0 -1
- package/dist/component/auditLog.js +0 -607
- package/dist/component/auditLog.js.map +0 -1
- package/dist/component/types.d.ts +0 -4
- package/dist/component/types.d.ts.map +0 -1
- package/dist/component/types.js +0 -2
- package/dist/component/types.js.map +0 -1
- package/src/cli/commands/admin.ts +0 -104
- package/src/cli/index.ts +0 -21
- package/src/cli/utils/detectConvexUrl.ts +0 -54
- package/src/cli/utils/openBrowser.ts +0 -16
- package/src/client/admin-config.ts +0 -138
- package/src/client/adminApi.ts +0 -942
- package/src/client/agentTools.ts +0 -1311
- package/src/client/argTypes.ts +0 -316
- package/src/client/field-types.ts +0 -187
- package/src/client/index.ts +0 -1301
- package/src/client/queryBuilder.ts +0 -1100
- package/src/client/schema/codegen.ts +0 -500
- package/src/client/schema/defineContentType.ts +0 -501
- package/src/client/schema/index.ts +0 -169
- package/src/client/schema/schemaDrift.ts +0 -574
- package/src/client/schema/typedClient.ts +0 -688
- package/src/client/schema/types.ts +0 -666
- package/src/client/types.ts +0 -723
- package/src/client/workflows.ts +0 -141
- package/src/client/wrapper.ts +0 -4304
- package/src/component/_generated/api.ts +0 -140
- package/src/component/_generated/component.ts +0 -5029
- package/src/component/_generated/dataModel.ts +0 -60
- package/src/component/_generated/server.ts +0 -156
- package/src/component/authorization.ts +0 -647
- package/src/component/authorizationHooks.ts +0 -668
- package/src/component/bulkOperations.ts +0 -687
- package/src/component/contentEntries.ts +0 -1976
- package/src/component/contentEntryMutations.ts +0 -1223
- package/src/component/contentEntryValidation.ts +0 -707
- package/src/component/contentLock.ts +0 -550
- package/src/component/contentTypeMigration.ts +0 -1064
- package/src/component/contentTypeMutations.ts +0 -969
- package/src/component/contentTypes.ts +0 -346
- package/src/component/convex.config.ts +0 -44
- package/src/component/documentTypes.ts +0 -240
- package/src/component/eventEmitter.ts +0 -485
- package/src/component/exportImport.ts +0 -1169
- package/src/component/index.ts +0 -491
- package/src/component/lib/deepReferenceResolver.ts +0 -999
- package/src/component/lib/errors.ts +0 -816
- package/src/component/lib/index.ts +0 -145
- package/src/component/lib/mediaReferenceResolver.ts +0 -495
- package/src/component/lib/metadataExtractor.ts +0 -792
- package/src/component/lib/mutationAuth.ts +0 -199
- package/src/component/lib/queries.ts +0 -79
- package/src/component/lib/ragContentChunker.ts +0 -1371
- package/src/component/lib/referenceResolver.ts +0 -430
- package/src/component/lib/slugGenerator.ts +0 -262
- package/src/component/lib/slugUniqueness.ts +0 -333
- package/src/component/lib/softDelete.ts +0 -44
- package/src/component/localeFallbackChain.ts +0 -673
- package/src/component/localeFields.ts +0 -896
- package/src/component/mediaAssetMutations.ts +0 -725
- package/src/component/mediaAssets.ts +0 -932
- package/src/component/mediaFolderMutations.ts +0 -1046
- package/src/component/mediaUploadMutations.ts +0 -224
- package/src/component/mediaVariantMutations.ts +0 -900
- package/src/component/mediaVariants.ts +0 -793
- package/src/component/ragContentIndexer.ts +0 -1067
- package/src/component/rateLimitHooks.ts +0 -572
- package/src/component/roles.ts +0 -1360
- package/src/component/scheduledPublish.ts +0 -358
- package/src/component/schema.ts +0 -617
- package/src/component/taxonomies.ts +0 -949
- package/src/component/taxonomyMutations.ts +0 -1210
- package/src/component/trash.ts +0 -724
- package/src/component/userContext.ts +0 -898
- package/src/component/validation.ts +0 -1388
- package/src/component/validators.ts +0 -949
- package/src/component/versionMutations.ts +0 -392
- package/src/component/webhookTrigger.ts +0 -1922
- package/src/react/index.ts +0 -898
- package/src/test.ts +0 -1580
|
@@ -1,949 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Taxonomy Query Functions
|
|
3
|
-
*
|
|
4
|
-
* Provides query functions for retrieving taxonomy definitions and terms.
|
|
5
|
-
* Taxonomies are classification systems (like tags, categories, topics) that
|
|
6
|
-
* can be applied to content entries for organization and filtering.
|
|
7
|
-
*
|
|
8
|
-
* Available queries:
|
|
9
|
-
* - `get`: Retrieve a single taxonomy by ID or name
|
|
10
|
-
* - `list`: List all taxonomies with optional filtering
|
|
11
|
-
* - `getTerm`: Retrieve a single term by ID or slug
|
|
12
|
-
* - `listTerms`: List terms within a taxonomy with filtering and search
|
|
13
|
-
* - `getTermsByEntry`: Get all terms associated with a content entry
|
|
14
|
-
* - `getEntriesByTerm`: Get content entries associated with a term
|
|
15
|
-
* - `suggestTerms`: Get term suggestions based on partial input
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { v } from "convex/values";
|
|
19
|
-
import { isDeleted } from "./lib/softDelete.js";
|
|
20
|
-
import { paginationOptsValidator } from "convex/server";
|
|
21
|
-
import { query } from "./_generated/server.js";
|
|
22
|
-
import { taxonomyDoc, taxonomyTermDoc } from "./validators.js";
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// Constants
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
const DEFAULT_NUM_ITEMS = 50;
|
|
29
|
-
const MAX_NUM_ITEMS = 250;
|
|
30
|
-
|
|
31
|
-
// =============================================================================
|
|
32
|
-
// Extended Validators
|
|
33
|
-
// =============================================================================
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Term with children for hierarchical display.
|
|
37
|
-
* Extends the base taxonomyTermDoc with a children array.
|
|
38
|
-
*/
|
|
39
|
-
const taxonomyTermWithChildren: ReturnType<typeof v.object> = v.object({
|
|
40
|
-
...taxonomyTermDoc.fields,
|
|
41
|
-
children: v.array(v.any()), // Recursive type - will contain taxonomyTermWithChildren
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// =============================================================================
|
|
45
|
-
// Get Taxonomy Query
|
|
46
|
-
// =============================================================================
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Query to retrieve a single taxonomy by ID or name.
|
|
50
|
-
*
|
|
51
|
-
* @param id - The taxonomy ID for direct lookup (most efficient)
|
|
52
|
-
* @param name - The machine-readable name for index-based lookup
|
|
53
|
-
* @param includeDeleted - Whether to return soft-deleted taxonomies (default: false)
|
|
54
|
-
*
|
|
55
|
-
* @returns The taxonomy document, or null if not found
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```typescript
|
|
59
|
-
* // Get by ID
|
|
60
|
-
* const taxonomy = await ctx.runQuery(api.taxonomies.get, { id: taxonomyId });
|
|
61
|
-
*
|
|
62
|
-
* // Get by name
|
|
63
|
-
* const tagsTaxonomy = await ctx.runQuery(api.taxonomies.get, { name: "tags" });
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
export const get = query({
|
|
67
|
-
args: {
|
|
68
|
-
id: v.optional(v.id("taxonomies")),
|
|
69
|
-
name: v.optional(v.string()),
|
|
70
|
-
includeDeleted: v.optional(v.boolean()),
|
|
71
|
-
},
|
|
72
|
-
returns: v.union(taxonomyDoc, v.null()),
|
|
73
|
-
handler: async (ctx, args) => {
|
|
74
|
-
const { id, name, includeDeleted = false } = args;
|
|
75
|
-
|
|
76
|
-
if (!id && !name) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let taxonomy;
|
|
81
|
-
|
|
82
|
-
if (id) {
|
|
83
|
-
taxonomy = await ctx.db.get(id);
|
|
84
|
-
} else if (name) {
|
|
85
|
-
taxonomy = await ctx.db
|
|
86
|
-
.query("taxonomies")
|
|
87
|
-
.withIndex("by_name", (q) => q.eq("name", name))
|
|
88
|
-
.first();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!taxonomy) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!includeDeleted && isDeleted(taxonomy)) {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return taxonomy;
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// =============================================================================
|
|
104
|
-
// List Taxonomies Query
|
|
105
|
-
// =============================================================================
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Query to list all taxonomies with optional filtering.
|
|
109
|
-
*
|
|
110
|
-
* @param isActive - Filter by active status
|
|
111
|
-
* @param isHierarchical - Filter by hierarchical type
|
|
112
|
-
* @param includeDeleted - Whether to include soft-deleted taxonomies
|
|
113
|
-
* @param paginationOpts - Standard Convex pagination options
|
|
114
|
-
*
|
|
115
|
-
* @returns Paginated list of taxonomy documents
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* ```typescript
|
|
119
|
-
* // List all active taxonomies
|
|
120
|
-
* const taxonomies = await ctx.runQuery(api.taxonomies.list, {
|
|
121
|
-
* isActive: true,
|
|
122
|
-
* paginationOpts: { numItems: 20 },
|
|
123
|
-
* });
|
|
124
|
-
*
|
|
125
|
-
* // List only flat taxonomies (like tags)
|
|
126
|
-
* const flatTaxonomies = await ctx.runQuery(api.taxonomies.list, {
|
|
127
|
-
* isHierarchical: false,
|
|
128
|
-
* paginationOpts: { numItems: 20 },
|
|
129
|
-
* });
|
|
130
|
-
* ```
|
|
131
|
-
*/
|
|
132
|
-
export const list = query({
|
|
133
|
-
args: {
|
|
134
|
-
isActive: v.optional(v.boolean()),
|
|
135
|
-
isHierarchical: v.optional(v.boolean()),
|
|
136
|
-
includeDeleted: v.optional(v.boolean()),
|
|
137
|
-
paginationOpts: v.optional(paginationOptsValidator),
|
|
138
|
-
},
|
|
139
|
-
returns: v.object({
|
|
140
|
-
page: v.array(taxonomyDoc),
|
|
141
|
-
continueCursor: v.union(v.string(), v.null()),
|
|
142
|
-
isDone: v.boolean(),
|
|
143
|
-
}),
|
|
144
|
-
handler: async (ctx, args) => {
|
|
145
|
-
const {
|
|
146
|
-
isActive,
|
|
147
|
-
isHierarchical,
|
|
148
|
-
includeDeleted = false,
|
|
149
|
-
paginationOpts,
|
|
150
|
-
} = args;
|
|
151
|
-
|
|
152
|
-
const numItems = paginationOpts
|
|
153
|
-
? Math.min(
|
|
154
|
-
Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
|
|
155
|
-
MAX_NUM_ITEMS,
|
|
156
|
-
)
|
|
157
|
-
: MAX_NUM_ITEMS;
|
|
158
|
-
|
|
159
|
-
let results;
|
|
160
|
-
|
|
161
|
-
if (isActive !== undefined) {
|
|
162
|
-
results = await ctx.db
|
|
163
|
-
.query("taxonomies")
|
|
164
|
-
.withIndex("by_active", (q) => q.eq("isActive", isActive))
|
|
165
|
-
.collect();
|
|
166
|
-
} else {
|
|
167
|
-
results = await ctx.db.query("taxonomies").collect();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Apply post-filters
|
|
171
|
-
if (!includeDeleted) {
|
|
172
|
-
results = results.filter((t) => !isDeleted(t));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (isHierarchical !== undefined) {
|
|
176
|
-
results = results.filter((t) => t.isHierarchical === isHierarchical);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Sort by sortOrder, then name
|
|
180
|
-
results.sort((a, b) => {
|
|
181
|
-
const orderA = a.sortOrder ?? 999;
|
|
182
|
-
const orderB = b.sortOrder ?? 999;
|
|
183
|
-
if (orderA !== orderB) return orderA - orderB;
|
|
184
|
-
return a.name.localeCompare(b.name);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Handle pagination
|
|
188
|
-
let startIndex = 0;
|
|
189
|
-
if (paginationOpts?.cursor) {
|
|
190
|
-
const cursorIndex = results.findIndex(
|
|
191
|
-
(t) => t._id === paginationOpts.cursor,
|
|
192
|
-
);
|
|
193
|
-
if (cursorIndex !== -1) {
|
|
194
|
-
startIndex = cursorIndex + 1;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const pageResults = results.slice(startIndex, startIndex + numItems + 1);
|
|
199
|
-
const isDone = pageResults.length <= numItems;
|
|
200
|
-
const page = isDone ? pageResults : pageResults.slice(0, numItems);
|
|
201
|
-
const continueCursor =
|
|
202
|
-
!isDone && page.length > 0 ? page[page.length - 1]._id : null;
|
|
203
|
-
|
|
204
|
-
return { page, continueCursor, isDone };
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// =============================================================================
|
|
209
|
-
// Get Term Query
|
|
210
|
-
// =============================================================================
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Query to retrieve a single taxonomy term by ID or slug.
|
|
214
|
-
*
|
|
215
|
-
* @param id - The term ID for direct lookup
|
|
216
|
-
* @param taxonomyId - The taxonomy ID (required when looking up by slug)
|
|
217
|
-
* @param slug - The term slug for lookup within a taxonomy
|
|
218
|
-
* @param includeDeleted - Whether to return soft-deleted terms
|
|
219
|
-
*
|
|
220
|
-
* @returns The term document, or null if not found
|
|
221
|
-
*/
|
|
222
|
-
export const getTerm = query({
|
|
223
|
-
args: {
|
|
224
|
-
id: v.optional(v.id("taxonomyTerms")),
|
|
225
|
-
taxonomyId: v.optional(v.id("taxonomies")),
|
|
226
|
-
slug: v.optional(v.string()),
|
|
227
|
-
includeDeleted: v.optional(v.boolean()),
|
|
228
|
-
},
|
|
229
|
-
returns: v.union(taxonomyTermDoc, v.null()),
|
|
230
|
-
handler: async (ctx, args) => {
|
|
231
|
-
const { id, taxonomyId, slug, includeDeleted = false } = args;
|
|
232
|
-
|
|
233
|
-
if (!id && (!taxonomyId || !slug)) {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
let term;
|
|
238
|
-
|
|
239
|
-
if (id) {
|
|
240
|
-
term = await ctx.db.get(id);
|
|
241
|
-
} else if (taxonomyId && slug) {
|
|
242
|
-
term = await ctx.db
|
|
243
|
-
.query("taxonomyTerms")
|
|
244
|
-
.withIndex("by_taxonomy_and_slug", (q) =>
|
|
245
|
-
q.eq("taxonomyId", taxonomyId).eq("slug", slug),
|
|
246
|
-
)
|
|
247
|
-
.first();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (!term) {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (!includeDeleted && isDeleted(term)) {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return term;
|
|
259
|
-
},
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// =============================================================================
|
|
263
|
-
// List Terms Query
|
|
264
|
-
// =============================================================================
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Query to list terms within a taxonomy.
|
|
268
|
-
*
|
|
269
|
-
* @param taxonomyId - The taxonomy to list terms from (required)
|
|
270
|
-
* @param parentId - Filter by parent term (for hierarchical navigation)
|
|
271
|
-
* @param rootOnly - Only return root-level terms (depth = 0)
|
|
272
|
-
* @param search - Search terms by name
|
|
273
|
-
* @param includeDeleted - Whether to include soft-deleted terms
|
|
274
|
-
* @param sortBy - Sort field: "name", "usageCount", "sortOrder"
|
|
275
|
-
* @param sortDirection - Sort direction
|
|
276
|
-
* @param paginationOpts - Standard Convex pagination options
|
|
277
|
-
*
|
|
278
|
-
* @returns Paginated list of term documents
|
|
279
|
-
*
|
|
280
|
-
* @example
|
|
281
|
-
* ```typescript
|
|
282
|
-
* // List all tags in a taxonomy
|
|
283
|
-
* const tags = await ctx.runQuery(api.taxonomies.listTerms, {
|
|
284
|
-
* taxonomyId: tagsTaxonomyId,
|
|
285
|
-
* paginationOpts: { numItems: 50 },
|
|
286
|
-
* });
|
|
287
|
-
*
|
|
288
|
-
* // List root categories only
|
|
289
|
-
* const rootCategories = await ctx.runQuery(api.taxonomies.listTerms, {
|
|
290
|
-
* taxonomyId: categoriesTaxonomyId,
|
|
291
|
-
* rootOnly: true,
|
|
292
|
-
* paginationOpts: { numItems: 20 },
|
|
293
|
-
* });
|
|
294
|
-
*
|
|
295
|
-
* // List children of a category
|
|
296
|
-
* const children = await ctx.runQuery(api.taxonomies.listTerms, {
|
|
297
|
-
* taxonomyId: categoriesTaxonomyId,
|
|
298
|
-
* parentId: parentCategoryId,
|
|
299
|
-
* paginationOpts: { numItems: 20 },
|
|
300
|
-
* });
|
|
301
|
-
*
|
|
302
|
-
* // Sort by popularity (usage count)
|
|
303
|
-
* const popularTags = await ctx.runQuery(api.taxonomies.listTerms, {
|
|
304
|
-
* taxonomyId: tagsTaxonomyId,
|
|
305
|
-
* sortBy: "usageCount",
|
|
306
|
-
* sortDirection: "desc",
|
|
307
|
-
* paginationOpts: { numItems: 20 },
|
|
308
|
-
* });
|
|
309
|
-
* ```
|
|
310
|
-
*/
|
|
311
|
-
export const listTerms = query({
|
|
312
|
-
args: {
|
|
313
|
-
taxonomyId: v.id("taxonomies"),
|
|
314
|
-
parentId: v.optional(v.id("taxonomyTerms")),
|
|
315
|
-
rootOnly: v.optional(v.boolean()),
|
|
316
|
-
search: v.optional(v.string()),
|
|
317
|
-
includeDeleted: v.optional(v.boolean()),
|
|
318
|
-
sortBy: v.optional(
|
|
319
|
-
v.union(
|
|
320
|
-
v.literal("name"),
|
|
321
|
-
v.literal("usageCount"),
|
|
322
|
-
v.literal("sortOrder"),
|
|
323
|
-
),
|
|
324
|
-
),
|
|
325
|
-
sortDirection: v.optional(v.union(v.literal("asc"), v.literal("desc"))),
|
|
326
|
-
paginationOpts: v.optional(paginationOptsValidator),
|
|
327
|
-
},
|
|
328
|
-
returns: v.object({
|
|
329
|
-
page: v.array(taxonomyTermDoc),
|
|
330
|
-
continueCursor: v.union(v.string(), v.null()),
|
|
331
|
-
isDone: v.boolean(),
|
|
332
|
-
}),
|
|
333
|
-
handler: async (ctx, args) => {
|
|
334
|
-
const {
|
|
335
|
-
taxonomyId,
|
|
336
|
-
parentId,
|
|
337
|
-
rootOnly,
|
|
338
|
-
search,
|
|
339
|
-
includeDeleted = false,
|
|
340
|
-
sortBy = "name",
|
|
341
|
-
sortDirection = "asc",
|
|
342
|
-
paginationOpts,
|
|
343
|
-
} = args;
|
|
344
|
-
|
|
345
|
-
const numItems = paginationOpts
|
|
346
|
-
? Math.min(
|
|
347
|
-
Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
|
|
348
|
-
MAX_NUM_ITEMS,
|
|
349
|
-
)
|
|
350
|
-
: MAX_NUM_ITEMS;
|
|
351
|
-
|
|
352
|
-
let results;
|
|
353
|
-
|
|
354
|
-
// Use search index if searching
|
|
355
|
-
if (search && search.trim().length > 0) {
|
|
356
|
-
results = await ctx.db
|
|
357
|
-
.query("taxonomyTerms")
|
|
358
|
-
.withSearchIndex("search_terms", (q) =>
|
|
359
|
-
q.search("searchText", search.trim()).eq("taxonomyId", taxonomyId),
|
|
360
|
-
)
|
|
361
|
-
.take(numItems * 4); // Fetch extra for post-filtering
|
|
362
|
-
} else if (parentId !== undefined) {
|
|
363
|
-
// Filter by parent
|
|
364
|
-
results = await ctx.db
|
|
365
|
-
.query("taxonomyTerms")
|
|
366
|
-
.withIndex("by_parent", (q) => q.eq("parentId", parentId))
|
|
367
|
-
.collect();
|
|
368
|
-
// Additional filter for taxonomy (parent could be cross-taxonomy in theory)
|
|
369
|
-
results = results.filter((t) => t.taxonomyId === taxonomyId);
|
|
370
|
-
} else {
|
|
371
|
-
// Get all terms in taxonomy
|
|
372
|
-
results = await ctx.db
|
|
373
|
-
.query("taxonomyTerms")
|
|
374
|
-
.withIndex("by_taxonomy", (q) => q.eq("taxonomyId", taxonomyId))
|
|
375
|
-
.collect();
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Apply post-filters
|
|
379
|
-
if (!includeDeleted) {
|
|
380
|
-
results = results.filter((t) => !isDeleted(t));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (rootOnly) {
|
|
384
|
-
results = results.filter((t) => t.depth === 0);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Sort results
|
|
388
|
-
results.sort((a, b) => {
|
|
389
|
-
let comparison = 0;
|
|
390
|
-
switch (sortBy) {
|
|
391
|
-
case "usageCount":
|
|
392
|
-
comparison = a.usageCount - b.usageCount;
|
|
393
|
-
break;
|
|
394
|
-
case "sortOrder":
|
|
395
|
-
comparison = (a.sortOrder ?? 999) - (b.sortOrder ?? 999);
|
|
396
|
-
break;
|
|
397
|
-
case "name":
|
|
398
|
-
default:
|
|
399
|
-
comparison = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
return sortDirection === "asc" ? comparison : -comparison;
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// Handle pagination
|
|
406
|
-
let startIndex = 0;
|
|
407
|
-
if (paginationOpts?.cursor) {
|
|
408
|
-
const cursorIndex = results.findIndex(
|
|
409
|
-
(t) => t._id === paginationOpts.cursor,
|
|
410
|
-
);
|
|
411
|
-
if (cursorIndex !== -1) {
|
|
412
|
-
startIndex = cursorIndex + 1;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const pageResults = results.slice(startIndex, startIndex + numItems + 1);
|
|
417
|
-
const isDone = pageResults.length <= numItems;
|
|
418
|
-
const page = isDone ? pageResults : pageResults.slice(0, numItems);
|
|
419
|
-
const continueCursor =
|
|
420
|
-
!isDone && page.length > 0 ? page[page.length - 1]._id : null;
|
|
421
|
-
|
|
422
|
-
return { page, continueCursor, isDone };
|
|
423
|
-
},
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
// =============================================================================
|
|
427
|
-
// Get Hierarchical Terms Query
|
|
428
|
-
// =============================================================================
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Query to get all terms in a taxonomy as a hierarchical tree structure.
|
|
432
|
-
*
|
|
433
|
-
* This is useful for rendering nested category selectors or tree views.
|
|
434
|
-
* Returns terms with their children nested in a tree structure.
|
|
435
|
-
*
|
|
436
|
-
* @param taxonomyId - The taxonomy to get terms from
|
|
437
|
-
* @param includeDeleted - Whether to include soft-deleted terms
|
|
438
|
-
*
|
|
439
|
-
* @returns Array of root terms with nested children
|
|
440
|
-
*
|
|
441
|
-
* @example
|
|
442
|
-
* ```typescript
|
|
443
|
-
* const tree = await ctx.runQuery(api.taxonomies.getTermsHierarchy, {
|
|
444
|
-
* taxonomyId: categoriesTaxonomyId,
|
|
445
|
-
* });
|
|
446
|
-
* // Returns: [
|
|
447
|
-
* // { name: "Tech", children: [{ name: "Web Dev", children: [...] }] },
|
|
448
|
-
* // { name: "Design", children: [...] },
|
|
449
|
-
* // ]
|
|
450
|
-
* ```
|
|
451
|
-
*/
|
|
452
|
-
export const getTermsHierarchy = query({
|
|
453
|
-
args: {
|
|
454
|
-
taxonomyId: v.id("taxonomies"),
|
|
455
|
-
includeDeleted: v.optional(v.boolean()),
|
|
456
|
-
},
|
|
457
|
-
returns: v.array(taxonomyTermWithChildren),
|
|
458
|
-
handler: async (ctx, args) => {
|
|
459
|
-
const { taxonomyId, includeDeleted = false } = args;
|
|
460
|
-
|
|
461
|
-
// Get all terms in the taxonomy
|
|
462
|
-
let terms = await ctx.db
|
|
463
|
-
.query("taxonomyTerms")
|
|
464
|
-
.withIndex("by_taxonomy", (q) => q.eq("taxonomyId", taxonomyId))
|
|
465
|
-
.collect();
|
|
466
|
-
|
|
467
|
-
// Filter deleted if needed
|
|
468
|
-
if (!includeDeleted) {
|
|
469
|
-
terms = terms.filter((t) => !isDeleted(t));
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Sort by sortOrder, then name
|
|
473
|
-
terms.sort((a, b) => {
|
|
474
|
-
const orderA = a.sortOrder ?? 999;
|
|
475
|
-
const orderB = b.sortOrder ?? 999;
|
|
476
|
-
if (orderA !== orderB) return orderA - orderB;
|
|
477
|
-
return a.name.localeCompare(b.name);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
// Build tree structure
|
|
481
|
-
const termMap = new Map<string, any>();
|
|
482
|
-
const rootTerms: any[] = [];
|
|
483
|
-
|
|
484
|
-
// First pass: create term objects with empty children
|
|
485
|
-
for (const term of terms) {
|
|
486
|
-
termMap.set(term._id, { ...term, children: [] });
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Second pass: link parents to children
|
|
490
|
-
for (const term of terms) {
|
|
491
|
-
const termWithChildren = termMap.get(term._id);
|
|
492
|
-
if (term.parentId && termMap.has(term.parentId)) {
|
|
493
|
-
const parent = termMap.get(term.parentId);
|
|
494
|
-
parent.children.push(termWithChildren);
|
|
495
|
-
} else {
|
|
496
|
-
rootTerms.push(termWithChildren);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
return rootTerms;
|
|
501
|
-
},
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// =============================================================================
|
|
505
|
-
// Suggest Terms Query
|
|
506
|
-
// =============================================================================
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Query to get term suggestions based on partial input.
|
|
510
|
-
*
|
|
511
|
-
* This is useful for autocomplete functionality when users are selecting
|
|
512
|
-
* or creating tags. Returns matching terms sorted by relevance and usage.
|
|
513
|
-
*
|
|
514
|
-
* @param taxonomyId - The taxonomy to search within
|
|
515
|
-
* @param query - The partial input to match against term names
|
|
516
|
-
* @param limit - Maximum number of suggestions to return (default: 10)
|
|
517
|
-
* @param excludeIds - Term IDs to exclude from suggestions (already selected)
|
|
518
|
-
*
|
|
519
|
-
* @returns Array of matching terms
|
|
520
|
-
*
|
|
521
|
-
* @example
|
|
522
|
-
* ```typescript
|
|
523
|
-
* const suggestions = await ctx.runQuery(api.taxonomies.suggestTerms, {
|
|
524
|
-
* taxonomyId: tagsTaxonomyId,
|
|
525
|
-
* query: "java",
|
|
526
|
-
* limit: 5,
|
|
527
|
-
* excludeIds: alreadySelectedTagIds,
|
|
528
|
-
* });
|
|
529
|
-
* // Returns: [{ name: "JavaScript" }, { name: "Java" }, { name: "JavaFX" }]
|
|
530
|
-
* ```
|
|
531
|
-
*/
|
|
532
|
-
export const suggestTerms = query({
|
|
533
|
-
args: {
|
|
534
|
-
taxonomyId: v.id("taxonomies"),
|
|
535
|
-
query: v.string(),
|
|
536
|
-
limit: v.optional(v.number()),
|
|
537
|
-
excludeIds: v.optional(v.array(v.id("taxonomyTerms"))),
|
|
538
|
-
},
|
|
539
|
-
returns: v.array(taxonomyTermDoc),
|
|
540
|
-
handler: async (ctx, args) => {
|
|
541
|
-
const {
|
|
542
|
-
taxonomyId,
|
|
543
|
-
query: searchQuery,
|
|
544
|
-
limit = 10,
|
|
545
|
-
excludeIds = [],
|
|
546
|
-
} = args;
|
|
547
|
-
|
|
548
|
-
const excludeSet = new Set(excludeIds);
|
|
549
|
-
|
|
550
|
-
if (!searchQuery || searchQuery.trim().length === 0) {
|
|
551
|
-
// Return popular terms if no query
|
|
552
|
-
const terms = await ctx.db
|
|
553
|
-
.query("taxonomyTerms")
|
|
554
|
-
.withIndex("by_taxonomy_and_usage", (q) =>
|
|
555
|
-
q.eq("taxonomyId", taxonomyId),
|
|
556
|
-
)
|
|
557
|
-
.order("desc")
|
|
558
|
-
.take(limit * 2);
|
|
559
|
-
|
|
560
|
-
return terms
|
|
561
|
-
.filter((t) => !isDeleted(t) && !excludeSet.has(t._id))
|
|
562
|
-
.slice(0, limit);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Search for matching terms
|
|
566
|
-
const terms = await ctx.db
|
|
567
|
-
.query("taxonomyTerms")
|
|
568
|
-
.withSearchIndex("search_terms", (q) =>
|
|
569
|
-
q.search("searchText", searchQuery.trim()).eq("taxonomyId", taxonomyId),
|
|
570
|
-
)
|
|
571
|
-
.take(limit * 2);
|
|
572
|
-
|
|
573
|
-
// Filter and limit
|
|
574
|
-
const filtered = terms.filter(
|
|
575
|
-
(t) => !isDeleted(t) && !excludeSet.has(t._id),
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
// Sort by: exact prefix match first, then usage count
|
|
579
|
-
const query = searchQuery.toLowerCase();
|
|
580
|
-
filtered.sort((a, b) => {
|
|
581
|
-
const aExact = a.name.toLowerCase().startsWith(query) ? 0 : 1;
|
|
582
|
-
const bExact = b.name.toLowerCase().startsWith(query) ? 0 : 1;
|
|
583
|
-
if (aExact !== bExact) return aExact - bExact;
|
|
584
|
-
return b.usageCount - a.usageCount;
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
return filtered.slice(0, limit);
|
|
588
|
-
},
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
// =============================================================================
|
|
592
|
-
// Get Terms by Entry Query
|
|
593
|
-
// =============================================================================
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* Query to get all taxonomy terms associated with a content entry.
|
|
597
|
-
*
|
|
598
|
-
* @param entryId - The content entry ID
|
|
599
|
-
* @param taxonomyId - Optional taxonomy filter
|
|
600
|
-
* @param fieldName - Optional field name filter
|
|
601
|
-
*
|
|
602
|
-
* @returns Array of terms associated with the entry
|
|
603
|
-
*
|
|
604
|
-
* @example
|
|
605
|
-
* ```typescript
|
|
606
|
-
* // Get all tags for an entry
|
|
607
|
-
* const entryTags = await ctx.runQuery(api.taxonomies.getTermsByEntry, {
|
|
608
|
-
* entryId: blogPostId,
|
|
609
|
-
* });
|
|
610
|
-
*
|
|
611
|
-
* // Get only tags from a specific field
|
|
612
|
-
* const primaryTags = await ctx.runQuery(api.taxonomies.getTermsByEntry, {
|
|
613
|
-
* entryId: blogPostId,
|
|
614
|
-
* fieldName: "tags",
|
|
615
|
-
* });
|
|
616
|
-
* ```
|
|
617
|
-
*/
|
|
618
|
-
export const getTermsByEntry = query({
|
|
619
|
-
args: {
|
|
620
|
-
entryId: v.id("contentEntries"),
|
|
621
|
-
taxonomyId: v.optional(v.id("taxonomies")),
|
|
622
|
-
fieldName: v.optional(v.string()),
|
|
623
|
-
},
|
|
624
|
-
returns: v.array(
|
|
625
|
-
v.object({
|
|
626
|
-
...taxonomyTermDoc.fields,
|
|
627
|
-
fieldName: v.string(),
|
|
628
|
-
sortOrder: v.optional(v.number()),
|
|
629
|
-
}),
|
|
630
|
-
),
|
|
631
|
-
handler: async (ctx, args) => {
|
|
632
|
-
const { entryId, taxonomyId, fieldName } = args;
|
|
633
|
-
|
|
634
|
-
// Get the junction table entries
|
|
635
|
-
const junctionQuery = ctx.db
|
|
636
|
-
.query("contentEntryTags")
|
|
637
|
-
.withIndex("by_entry", (q) => q.eq("entryId", entryId));
|
|
638
|
-
|
|
639
|
-
const junctionEntries = await junctionQuery.collect();
|
|
640
|
-
|
|
641
|
-
// Filter by taxonomy or field if specified
|
|
642
|
-
let filtered = junctionEntries;
|
|
643
|
-
if (taxonomyId) {
|
|
644
|
-
filtered = filtered.filter((j) => j.taxonomyId === taxonomyId);
|
|
645
|
-
}
|
|
646
|
-
if (fieldName) {
|
|
647
|
-
filtered = filtered.filter((j) => j.fieldName === fieldName);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Sort by sortOrder
|
|
651
|
-
filtered.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
652
|
-
|
|
653
|
-
// Fetch the actual terms
|
|
654
|
-
const results = [];
|
|
655
|
-
for (const junction of filtered) {
|
|
656
|
-
const term = await ctx.db.get(junction.termId);
|
|
657
|
-
if (term && !isDeleted(term)) {
|
|
658
|
-
results.push({
|
|
659
|
-
...term,
|
|
660
|
-
fieldName: junction.fieldName,
|
|
661
|
-
sortOrder: junction.sortOrder,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
return results;
|
|
667
|
-
},
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
// =============================================================================
|
|
671
|
-
// Get Entries by Term Query
|
|
672
|
-
// =============================================================================
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Query to get content entries that have a specific term.
|
|
676
|
-
*
|
|
677
|
-
* @param termId - The term ID to search for
|
|
678
|
-
* @param status - Optional entry status filter
|
|
679
|
-
* @param paginationOpts - Standard Convex pagination options
|
|
680
|
-
*
|
|
681
|
-
* @returns Paginated list of entry IDs with the term
|
|
682
|
-
*
|
|
683
|
-
* @example
|
|
684
|
-
* ```typescript
|
|
685
|
-
* // Get all entries with a specific tag
|
|
686
|
-
* const entriesWithTag = await ctx.runQuery(api.taxonomies.getEntriesByTerm, {
|
|
687
|
-
* termId: javascriptTagId,
|
|
688
|
-
* status: "published",
|
|
689
|
-
* paginationOpts: { numItems: 20 },
|
|
690
|
-
* });
|
|
691
|
-
* ```
|
|
692
|
-
*/
|
|
693
|
-
export const getEntriesByTerm = query({
|
|
694
|
-
args: {
|
|
695
|
-
termId: v.id("taxonomyTerms"),
|
|
696
|
-
status: v.optional(
|
|
697
|
-
v.union(
|
|
698
|
-
v.literal("draft"),
|
|
699
|
-
v.literal("published"),
|
|
700
|
-
v.literal("archived"),
|
|
701
|
-
v.literal("scheduled"),
|
|
702
|
-
),
|
|
703
|
-
),
|
|
704
|
-
paginationOpts: v.optional(paginationOptsValidator),
|
|
705
|
-
},
|
|
706
|
-
returns: v.object({
|
|
707
|
-
page: v.array(v.id("contentEntries")),
|
|
708
|
-
continueCursor: v.union(v.string(), v.null()),
|
|
709
|
-
isDone: v.boolean(),
|
|
710
|
-
}),
|
|
711
|
-
handler: async (ctx, args) => {
|
|
712
|
-
const { termId, status, paginationOpts } = args;
|
|
713
|
-
|
|
714
|
-
const numItems = paginationOpts
|
|
715
|
-
? Math.min(
|
|
716
|
-
Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
|
|
717
|
-
MAX_NUM_ITEMS,
|
|
718
|
-
)
|
|
719
|
-
: DEFAULT_NUM_ITEMS;
|
|
720
|
-
|
|
721
|
-
// Get junction entries for this term
|
|
722
|
-
const junctionEntries = await ctx.db
|
|
723
|
-
.query("contentEntryTags")
|
|
724
|
-
.withIndex("by_term", (q) => q.eq("termId", termId))
|
|
725
|
-
.collect();
|
|
726
|
-
|
|
727
|
-
// Get unique entry IDs
|
|
728
|
-
const entryIds = [...new Set(junctionEntries.map((j) => j.entryId))];
|
|
729
|
-
|
|
730
|
-
// Filter by status if needed
|
|
731
|
-
let filteredEntryIds = entryIds;
|
|
732
|
-
if (status) {
|
|
733
|
-
const validEntryIds: typeof entryIds = [];
|
|
734
|
-
for (const entryId of entryIds) {
|
|
735
|
-
const entry = await ctx.db.get(entryId);
|
|
736
|
-
if (entry && entry.status === status && !isDeleted(entry)) {
|
|
737
|
-
validEntryIds.push(entryId);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
filteredEntryIds = validEntryIds;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Handle pagination
|
|
744
|
-
let startIndex = 0;
|
|
745
|
-
if (paginationOpts?.cursor) {
|
|
746
|
-
const cursorIndex = filteredEntryIds.findIndex(
|
|
747
|
-
(id) => id === paginationOpts.cursor,
|
|
748
|
-
);
|
|
749
|
-
if (cursorIndex !== -1) {
|
|
750
|
-
startIndex = cursorIndex + 1;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
const pageResults = filteredEntryIds.slice(
|
|
755
|
-
startIndex,
|
|
756
|
-
startIndex + numItems + 1,
|
|
757
|
-
);
|
|
758
|
-
const isDone = pageResults.length <= numItems;
|
|
759
|
-
const page = isDone ? pageResults : pageResults.slice(0, numItems);
|
|
760
|
-
const continueCursor =
|
|
761
|
-
!isDone && page.length > 0 ? page[page.length - 1] : null;
|
|
762
|
-
|
|
763
|
-
return { page, continueCursor, isDone };
|
|
764
|
-
},
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
// =============================================================================
|
|
768
|
-
// Count Terms Query
|
|
769
|
-
// =============================================================================
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Query to count terms in a taxonomy.
|
|
773
|
-
*
|
|
774
|
-
* @param taxonomyId - The taxonomy to count terms in
|
|
775
|
-
* @param includeDeleted - Whether to include soft-deleted terms
|
|
776
|
-
*
|
|
777
|
-
* @returns Object containing the count
|
|
778
|
-
*/
|
|
779
|
-
export const countTerms = query({
|
|
780
|
-
args: {
|
|
781
|
-
taxonomyId: v.id("taxonomies"),
|
|
782
|
-
includeDeleted: v.optional(v.boolean()),
|
|
783
|
-
},
|
|
784
|
-
returns: v.object({
|
|
785
|
-
count: v.number(),
|
|
786
|
-
}),
|
|
787
|
-
handler: async (ctx, args) => {
|
|
788
|
-
const { taxonomyId, includeDeleted = false } = args;
|
|
789
|
-
|
|
790
|
-
const terms = await ctx.db
|
|
791
|
-
.query("taxonomyTerms")
|
|
792
|
-
.withIndex("by_taxonomy", (q) => q.eq("taxonomyId", taxonomyId))
|
|
793
|
-
.collect();
|
|
794
|
-
|
|
795
|
-
const filteredTerms = includeDeleted
|
|
796
|
-
? terms
|
|
797
|
-
: terms.filter((t) => !isDeleted(t));
|
|
798
|
-
|
|
799
|
-
return { count: filteredTerms.length };
|
|
800
|
-
},
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
// =============================================================================
|
|
804
|
-
// Media Asset Taxonomy Queries
|
|
805
|
-
// =============================================================================
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Query to get all taxonomy terms associated with a media asset.
|
|
809
|
-
*
|
|
810
|
-
* @param mediaId - The media asset ID
|
|
811
|
-
* @param taxonomyId - Optional taxonomy filter
|
|
812
|
-
*
|
|
813
|
-
* @returns Array of terms associated with the media asset
|
|
814
|
-
*
|
|
815
|
-
* @example
|
|
816
|
-
* ```typescript
|
|
817
|
-
* // Get all terms for a media asset
|
|
818
|
-
* const mediaTags = await ctx.runQuery(api.taxonomies.getTermsByMedia, {
|
|
819
|
-
* mediaId: imageId,
|
|
820
|
-
* });
|
|
821
|
-
*
|
|
822
|
-
* // Get only terms from a specific taxonomy
|
|
823
|
-
* const categories = await ctx.runQuery(api.taxonomies.getTermsByMedia, {
|
|
824
|
-
* mediaId: imageId,
|
|
825
|
-
* taxonomyId: categoriesTaxonomyId,
|
|
826
|
-
* });
|
|
827
|
-
* ```
|
|
828
|
-
*/
|
|
829
|
-
export const getTermsByMedia = query({
|
|
830
|
-
args: {
|
|
831
|
-
mediaId: v.id("mediaItems"),
|
|
832
|
-
taxonomyId: v.optional(v.id("taxonomies")),
|
|
833
|
-
},
|
|
834
|
-
returns: v.array(
|
|
835
|
-
v.object({
|
|
836
|
-
...taxonomyTermDoc.fields,
|
|
837
|
-
sortOrder: v.optional(v.number()),
|
|
838
|
-
}),
|
|
839
|
-
),
|
|
840
|
-
handler: async (ctx, args) => {
|
|
841
|
-
const { mediaId, taxonomyId } = args;
|
|
842
|
-
|
|
843
|
-
const junctionEntries = await ctx.db
|
|
844
|
-
.query("mediaAssetTags")
|
|
845
|
-
.withIndex("by_media", (q) => q.eq("mediaId", mediaId))
|
|
846
|
-
.collect();
|
|
847
|
-
|
|
848
|
-
let filtered = junctionEntries;
|
|
849
|
-
if (taxonomyId) {
|
|
850
|
-
filtered = filtered.filter((j) => j.taxonomyId === taxonomyId);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
filtered.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
854
|
-
|
|
855
|
-
const results = [];
|
|
856
|
-
for (const junction of filtered) {
|
|
857
|
-
const term = await ctx.db.get(junction.termId);
|
|
858
|
-
if (term && !isDeleted(term)) {
|
|
859
|
-
results.push({
|
|
860
|
-
...term,
|
|
861
|
-
sortOrder: junction.sortOrder,
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
return results;
|
|
867
|
-
},
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* Query to get media assets that have a specific term.
|
|
872
|
-
*
|
|
873
|
-
* @param termId - The term ID to search for
|
|
874
|
-
* @param includeDeleted - Whether to include soft-deleted media
|
|
875
|
-
* @param paginationOpts - Standard Convex pagination options
|
|
876
|
-
*
|
|
877
|
-
* @returns Paginated list of media asset IDs with the term
|
|
878
|
-
*
|
|
879
|
-
* @example
|
|
880
|
-
* ```typescript
|
|
881
|
-
* // Get all media with a specific category
|
|
882
|
-
* const mediaWithCategory = await ctx.runQuery(api.taxonomies.getMediaByTerm, {
|
|
883
|
-
* termId: landscapeCategoryId,
|
|
884
|
-
* paginationOpts: { numItems: 20 },
|
|
885
|
-
* });
|
|
886
|
-
* ```
|
|
887
|
-
*/
|
|
888
|
-
export const getMediaByTerm = query({
|
|
889
|
-
args: {
|
|
890
|
-
termId: v.id("taxonomyTerms"),
|
|
891
|
-
includeDeleted: v.optional(v.boolean()),
|
|
892
|
-
paginationOpts: v.optional(paginationOptsValidator),
|
|
893
|
-
},
|
|
894
|
-
returns: v.object({
|
|
895
|
-
page: v.array(v.id("mediaItems")),
|
|
896
|
-
continueCursor: v.union(v.string(), v.null()),
|
|
897
|
-
isDone: v.boolean(),
|
|
898
|
-
}),
|
|
899
|
-
handler: async (ctx, args) => {
|
|
900
|
-
const { termId, includeDeleted = false, paginationOpts } = args;
|
|
901
|
-
|
|
902
|
-
const numItems = paginationOpts
|
|
903
|
-
? Math.min(
|
|
904
|
-
Math.max(1, paginationOpts.numItems ?? DEFAULT_NUM_ITEMS),
|
|
905
|
-
MAX_NUM_ITEMS,
|
|
906
|
-
)
|
|
907
|
-
: DEFAULT_NUM_ITEMS;
|
|
908
|
-
|
|
909
|
-
const junctionEntries = await ctx.db
|
|
910
|
-
.query("mediaAssetTags")
|
|
911
|
-
.withIndex("by_term", (q) => q.eq("termId", termId))
|
|
912
|
-
.collect();
|
|
913
|
-
|
|
914
|
-
const mediaIds = [...new Set(junctionEntries.map((j) => j.mediaId))];
|
|
915
|
-
|
|
916
|
-
let filteredMediaIds = mediaIds;
|
|
917
|
-
if (!includeDeleted) {
|
|
918
|
-
const validMediaIds: typeof mediaIds = [];
|
|
919
|
-
for (const mediaId of mediaIds) {
|
|
920
|
-
const media = await ctx.db.get(mediaId);
|
|
921
|
-
if (media && !isDeleted(media)) {
|
|
922
|
-
validMediaIds.push(mediaId);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
filteredMediaIds = validMediaIds;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
let startIndex = 0;
|
|
929
|
-
if (paginationOpts?.cursor) {
|
|
930
|
-
const cursorIndex = filteredMediaIds.findIndex(
|
|
931
|
-
(id) => id === paginationOpts.cursor,
|
|
932
|
-
);
|
|
933
|
-
if (cursorIndex !== -1) {
|
|
934
|
-
startIndex = cursorIndex + 1;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const pageResults = filteredMediaIds.slice(
|
|
939
|
-
startIndex,
|
|
940
|
-
startIndex + numItems + 1,
|
|
941
|
-
);
|
|
942
|
-
const isDone = pageResults.length <= numItems;
|
|
943
|
-
const page = isDone ? pageResults : pageResults.slice(0, numItems);
|
|
944
|
-
const continueCursor =
|
|
945
|
-
!isDone && page.length > 0 ? page[page.length - 1] : null;
|
|
946
|
-
|
|
947
|
-
return { page, continueCursor, isDone };
|
|
948
|
-
},
|
|
949
|
-
});
|