@meeovi/directus-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@meeovi/directus-client",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "type": "commonjs",
13
+ "dependencies": {
14
+ "@directus/sdk": "^18.0.3",
15
+ "@directus/types": "^14.0.0",
16
+ "@directus/visual-editing": "^1.1.0",
17
+ "react": "^19.2.3",
18
+ "vue": "^3.5.24"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^19.2.9"
22
+ }
23
+ }
@@ -0,0 +1,45 @@
1
+ // src/client/createClient.ts
2
+
3
+ import {
4
+ createDirectus,
5
+ rest,
6
+ authentication,
7
+ readItem,
8
+ readItems,
9
+ createItem,
10
+ updateItem,
11
+ deleteItem,
12
+ uploadFiles,
13
+ readSingleton,
14
+ readFieldsByCollection
15
+ } from '@directus/sdk';
16
+
17
+ export interface MeeoviDirectusClient<Schema> {
18
+ client: ReturnType<typeof createDirectus<Schema>>;
19
+ readItem: typeof readItem;
20
+ readItems: typeof readItems;
21
+ createItem: typeof createItem;
22
+ updateItem: typeof updateItem;
23
+ deleteItem: typeof deleteItem;
24
+ uploadFiles: typeof uploadFiles;
25
+ readSingleton: typeof readSingleton;
26
+ readFieldsByCollection: typeof readFieldsByCollection;
27
+ }
28
+
29
+ export function createMeeoviDirectusClient<Schema>(url: string): MeeoviDirectusClient<Schema> {
30
+ const client = createDirectus<Schema>(url)
31
+ .with(rest())
32
+ .with(authentication());
33
+
34
+ return {
35
+ client,
36
+ readItem,
37
+ readItems,
38
+ createItem,
39
+ updateItem,
40
+ deleteItem,
41
+ uploadFiles,
42
+ readSingleton,
43
+ readFieldsByCollection
44
+ };
45
+ }
@@ -0,0 +1,89 @@
1
+ import type { DirectusField } from '../schema/types';
2
+
3
+ export interface FormEngineOptions {
4
+ clearOnSuccess?: boolean;
5
+ onSuccess?: () => void;
6
+ onError?: (msg: string) => void;
7
+ }
8
+
9
+ export function createFormEngine(
10
+ collectionName: string,
11
+ fields: DirectusField[],
12
+ directusClient: any,
13
+ opts?: FormEngineOptions
14
+ ) {
15
+ const form: Record<string, any> = {};
16
+ let error: string | null = null;
17
+ let success: string | null = null;
18
+
19
+ const validate = () => {
20
+ error = null;
21
+
22
+ for (const field of fields) {
23
+ const meta = field;
24
+ if (!meta?.validation) continue;
25
+
26
+ try {
27
+ const validation = meta.validation;
28
+
29
+ if (validation._and) {
30
+ for (const rule of validation._and) {
31
+ const fieldName = Object.keys(rule)[0];
32
+ if (!fieldName) continue;
33
+
34
+ const ruleDef = (rule as any)[fieldName];
35
+
36
+ if (ruleDef?._regex) {
37
+ const regex = new RegExp(ruleDef._regex);
38
+ const value = String(form[field.field] ?? '');
39
+
40
+ if (!regex.test(value)) {
41
+ error = meta.validation_message || `${field.field} failed validation`;
42
+ return false;
43
+ }
44
+ }
45
+ }
46
+ }
47
+ } catch {
48
+ error = `Validation error for ${field.field}`;
49
+ return false;
50
+ }
51
+ }
52
+
53
+ return true;
54
+ };
55
+
56
+ const submit = async () => {
57
+ if (!validate()) return { error, success };
58
+
59
+ const result = await directusClient.request(
60
+ directusClient.createItem(collectionName, form)
61
+ );
62
+
63
+ if (result?.error) {
64
+ error = result.error.message;
65
+ return { error, success };
66
+ }
67
+
68
+ success = `${collectionName} created successfully`;
69
+
70
+ if (opts?.clearOnSuccess) {
71
+ for (const key of Object.keys(form)) delete form[key];
72
+ }
73
+
74
+ opts?.onSuccess?.();
75
+
76
+ return { error, success };
77
+ };
78
+
79
+ return {
80
+ form,
81
+ submit,
82
+ get error() {
83
+ return error;
84
+ },
85
+ get success() {
86
+ return success;
87
+ }
88
+ };
89
+ }
@@ -0,0 +1,11 @@
1
+ import type { DirectusField } from '../schema/types';
2
+
3
+ export function generateTableSchema(fields: DirectusField[]) {
4
+ return fields
5
+ .filter(f => !f.hidden)
6
+ .map(f => ({
7
+ key: f.field,
8
+ label: f.field.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
9
+ type: f.type
10
+ }));
11
+ }
@@ -0,0 +1,29 @@
1
+ import type { DirectusField } from '../schema/types';
2
+
3
+ export function validateField(field: DirectusField, value: any): string | null {
4
+ if (!field.validation) return null;
5
+
6
+ try {
7
+ const validation = field.validation;
8
+
9
+ if (validation._and) {
10
+ for (const rule of validation._and) {
11
+ const fieldName = Object.keys(rule)[0];
12
+ if (!fieldName) continue;
13
+
14
+ const ruleDef = (rule as any)[fieldName];
15
+
16
+ if (ruleDef?._regex) {
17
+ const regex = new RegExp(ruleDef._regex);
18
+ if (!regex.test(String(value ?? ''))) {
19
+ return field.validation_message || `${field.field} failed validation`;
20
+ }
21
+ }
22
+ }
23
+ }
24
+ } catch {
25
+ return `Validation error for ${field.field}`;
26
+ }
27
+
28
+ return null;
29
+ }
@@ -0,0 +1,7 @@
1
+ export const widgetRegistry = {
2
+ text: { component: 'TextInput' },
3
+ number: { component: 'NumberInput' },
4
+ checkbox: { component: 'CheckboxInput' },
5
+ date: { component: 'DateInput' },
6
+ json: { component: 'JsonEditor' }
7
+ };
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // src/index.ts
2
+
3
+ export * from './client/createClient';
4
+
5
+ // Vue bindings
6
+ export { default as DirectusVueProvider } from './vue/DirectusProvider';
7
+ export * from './vue/useDirectus';
8
+
9
+ // React bindings
10
+ export { DirectusProvider as DirectusReactProvider } from './react/DirectusProvider';
11
+ export * from './react/useDirectus';
12
+
13
+ // Schema + generators + utils
14
+ export * from './schema/types';
15
+ export * from './schema/introspect';
16
+ export * from './utils/collections';
17
+ export * from './utils/fields';
18
+ export * from './generators/form-engine';
19
+ export * from './generators/table-engine';
20
+ export * from './generators/validation-engine';
21
+ export * from './generators/widget-registry';
@@ -0,0 +1,18 @@
1
+ import React, { createContext } from 'react';
2
+ import type { MeeoviDirectusClient } from '../client/createClient';
3
+
4
+ export const DirectusContext = createContext<MeeoviDirectusClient<any> | null>(null);
5
+
6
+ export function DirectusProvider({
7
+ client,
8
+ children
9
+ }: {
10
+ client: MeeoviDirectusClient<any>;
11
+ children: React.ReactNode;
12
+ }) {
13
+ return (
14
+ <DirectusContext.Provider value={client}>
15
+ {children}
16
+ </DirectusContext.Provider>
17
+ );
18
+ }
@@ -0,0 +1,13 @@
1
+ import { useContext } from 'react';
2
+ import { DirectusContext } from './DirectusProvider';
3
+ import type { MeeoviDirectusClient } from '../client/createClient';
4
+
5
+ export function useRDirectus<Schema>() {
6
+ const client = useContext(DirectusContext) as MeeoviDirectusClient<Schema> | null;
7
+
8
+ if (!client) {
9
+ throw new Error('Directus client not provided. Wrap your app in <DirectusProvider>.');
10
+ }
11
+
12
+ return client;
13
+ }
@@ -0,0 +1,47 @@
1
+ // schema/introspect.ts
2
+
3
+ import {
4
+ readFieldsByCollection,
5
+ readItems
6
+ } from '@directus/sdk';
7
+ import type {
8
+ DirectusSchema,
9
+ DirectusField,
10
+ DirectusCollection,
11
+ DirectusRelation,
12
+ } from './types';
13
+
14
+ export async function introspectCollections(client: any): Promise<DirectusCollection[]> {
15
+ return await client.request((readItems as any)('directus_collection'));
16
+ }
17
+
18
+ export async function introspectFields(client: any, collection: string): Promise<DirectusField[]> {
19
+ return await client.request((readFieldsByCollection as any)(collection));
20
+ }
21
+
22
+ export async function introspectRelations(client: any): Promise<DirectusRelation[]> {
23
+ return await client.request((readItems as any)('directus_relations'));
24
+ }
25
+
26
+ export async function introspectSchema(client: any): Promise<DirectusSchema> {
27
+ const [collections, relations] = await Promise.all([
28
+ introspectCollections(client),
29
+ introspectRelations(client),
30
+ ]);
31
+
32
+ const fields: DirectusField[] = [];
33
+
34
+ for (const col of collections) {
35
+ const colFields = await introspectFields(client, col.collection);
36
+ fields.push(...colFields);
37
+ }
38
+
39
+ return {
40
+ collections,
41
+ fields,
42
+ relations,
43
+ directus_collections: collections,
44
+ directus_fields: fields,
45
+ directus_relations: relations,
46
+ };
47
+ }
@@ -0,0 +1,59 @@
1
+ export interface DirectusField {
2
+ collection: string;
3
+ field: string;
4
+ type: string;
5
+ interface ? : string;
6
+ options ? : Record < string,
7
+ any > ;
8
+ required ? : boolean;
9
+ readonly ? : boolean;
10
+ hidden ? : boolean;
11
+ sort ? : number;
12
+ special ? : string[];
13
+ validation ? : Record < string,
14
+ any > ;
15
+ validation_message ? : string;
16
+ }
17
+
18
+ export interface DirectusRelation {
19
+ collection: string;
20
+ field: string;
21
+ related_collection: string | null;
22
+ meta ? : Record < string,
23
+ any > ;
24
+ }
25
+
26
+ export interface DirectusCollection {
27
+ collection: string;
28
+ meta ? : Record < string,
29
+ any > ;
30
+ schema ? : Record < string,
31
+ any > ;
32
+ }
33
+
34
+ export interface DirectusSchema {
35
+ directus_collections: DirectusCollection[];
36
+ directus_relations: DirectusRelation[];
37
+ directus_fields: DirectusField[];
38
+ collections: DirectusCollection[];
39
+ fields: DirectusField[];
40
+ relations: DirectusRelation[];
41
+ }
42
+
43
+ export interface GeneratedFieldSchema {
44
+ key: string;
45
+ type: string;
46
+ widget: string;
47
+ required: boolean;
48
+ readonly: boolean;
49
+ hidden: boolean;
50
+ options ? : Record < string,
51
+ any > ;
52
+ validation ? : Record < string,
53
+ any > ;
54
+ }
55
+
56
+ export interface GeneratedCollectionSchema {
57
+ collection: string;
58
+ fields: GeneratedFieldSchema[];
59
+ }
@@ -0,0 +1,18 @@
1
+ import type { DirectusSchema, DirectusCollection, DirectusField } from '../schema/types';
2
+
3
+ export function getCollection(schema: DirectusSchema, name: string): DirectusCollection | undefined {
4
+ return schema.collections.find(c => c.collection === name);
5
+ }
6
+
7
+ export function getCollectionFields(schema: DirectusSchema, name: string): DirectusField[] {
8
+ return schema.fields.filter(f => f.collection === name);
9
+ }
10
+
11
+ export function listCollections(schema: DirectusSchema): string[] {
12
+ return schema.collections.map(c => c.collection);
13
+ }
14
+
15
+ export function isSingleton(schema: DirectusSchema, name: string): boolean {
16
+ const col = getCollection(schema, name);
17
+ return col?.meta?.singleton === true;
18
+ }
@@ -0,0 +1,52 @@
1
+ import type {
2
+ DirectusField,
3
+ GeneratedFieldSchema,
4
+ } from '../schema/types';
5
+
6
+ export function mapFieldToWidget(field: DirectusField): string {
7
+ if (field.interface) return field.interface;
8
+
9
+ switch (field.type) {
10
+ case 'string':
11
+ case 'text':
12
+ return 'text';
13
+
14
+ case 'integer':
15
+ case 'bigInteger':
16
+ case 'float':
17
+ case 'decimal':
18
+ return 'number';
19
+
20
+ case 'boolean':
21
+ return 'checkbox';
22
+
23
+ case 'date':
24
+ case 'dateTime':
25
+ return 'date';
26
+
27
+ case 'json':
28
+ return 'json';
29
+
30
+ default:
31
+ return 'text';
32
+ }
33
+ }
34
+
35
+ export function generateFieldSchema(field: DirectusField): GeneratedFieldSchema {
36
+ return {
37
+ key: field.field,
38
+ type: field.type,
39
+ widget: mapFieldToWidget(field),
40
+ required: field.required ?? false,
41
+ readonly: field.readonly ?? false,
42
+ hidden: field.hidden ?? false,
43
+ options: field.options ?? {},
44
+ validation: field.validation ?? {},
45
+ };
46
+ }
47
+
48
+ export function generateFieldsSchema(fields: DirectusField[]): GeneratedFieldSchema[] {
49
+ return fields
50
+ .sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
51
+ .map(generateFieldSchema);
52
+ }
@@ -0,0 +1,144 @@
1
+ import { ref, computed, watch } from "vue"
2
+ import Input from "~/components/ui/forms/TextInput.vue"
3
+ import TextArea from "~/components/ui/forms/TextArea.vue"
4
+ import Select from "~/components/ui/forms/SelectInput.vue"
5
+ import DateTime from "~/components/ui/forms/DateTime.vue"
6
+ import FileInput from "~/components/ui/forms/FileInput.vue"
7
+ import BooleanInput from "~/components/ui/forms/BooleanInput.vue"
8
+ import RelationSelect from "~/components/ui/forms/RelationSelect.vue"
9
+ import RepeaterInput from "~/components/ui/forms/RepeaterInput.vue"
10
+ import TiptapEditor from "~/components/ui/forms/TiptapEditor.vue"
11
+
12
+ export function useDirectusField(field: any, modelValue: any, emit: any, formContext?: any) {
13
+ const internalValue = computed({
14
+ get: () => modelValue,
15
+ set: (v) => emit("update:modelValue", v),
16
+ })
17
+
18
+ const previousAuto = ref<string | null>(null)
19
+ const internalAuto = computed(() => {
20
+ return previousAuto.value != null && internalValue.value === previousAuto.value
21
+ })
22
+
23
+ const slugify = (val: string) =>
24
+ String(val || "")
25
+ .toLowerCase()
26
+ .normalize("NFKD")
27
+ .replace(/\s+/g, "-")
28
+ .replace(/[^a-z0-9\-]/g, "")
29
+ .replace(/-+/g, "-")
30
+ .replace(/^-|-$/g, "")
31
+
32
+ const isAliasField = computed(() => {
33
+ const meta = field?.meta ?? {}
34
+ return (
35
+ meta.interface === "alias" ||
36
+ (meta.options && (meta.options.source || meta.options.from))
37
+ )
38
+ })
39
+
40
+ if (isAliasField.value && formContext && formContext.form) {
41
+ const meta = field?.meta ?? {}
42
+ let sourceField: string | null = meta?.options?.source ?? meta?.options?.from ?? null
43
+
44
+ if (!sourceField && formContext.fields) {
45
+ const candidates = ["name", "title"]
46
+ sourceField =
47
+ candidates.find((c: string) =>
48
+ formContext.fields.value.some((f: any) => f.field === c)
49
+ ) ?? null
50
+ }
51
+
52
+ if (sourceField) {
53
+ watch(
54
+ () => formContext.form.value?.[sourceField as string],
55
+ (newVal) => {
56
+ const target = field.field
57
+ const current = formContext.form.value?.[target] ?? ""
58
+ const generated = slugify(String(newVal ?? ""))
59
+
60
+ if (!generated) return
61
+
62
+ // Case 1: Empty or previously auto‑generated → regenerate
63
+ if (!current || current === previousAuto.value) {
64
+ formContext.form.value[target] = generated
65
+ internalValue.value = generated
66
+ previousAuto.value = generated
67
+ }
68
+ },
69
+ { immediate: true }
70
+ )
71
+
72
+ // Watch the target itself: if cleared, regenerate
73
+ watch(
74
+ () => formContext.form.value?.[field.field],
75
+ (newVal) => {
76
+ if (!newVal) {
77
+ const sourceVal = formContext.form.value?.[sourceField as string] ?? ""
78
+ const regenerated = slugify(String(sourceVal))
79
+ if (regenerated) {
80
+ formContext.form.value[field.field] = regenerated
81
+ internalValue.value = regenerated
82
+ previousAuto.value = regenerated
83
+ }
84
+ }
85
+ }
86
+ )
87
+ }
88
+ }
89
+
90
+ // --- Label prettifier ---
91
+ const prettify = (s: string) =>
92
+ s.replace(/_/g, " ")
93
+ .split(" ")
94
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
95
+ .join(" ")
96
+
97
+ const label = computed(() => {
98
+ const raw = field?.meta?.field ?? field?.field ?? ""
99
+ return prettify(raw)
100
+ })
101
+
102
+ // --- Field lookup ---
103
+ const fieldLookup = computed(() => {
104
+ const iface = field?.meta?.interface
105
+ const name = String(iface ?? "").toLowerCase()
106
+
107
+ if (["input", "string"].includes(name)) return Input
108
+ if (
109
+ ["textarea", "wysiwyg", "rich-text", "input-multiline", "input-rich-text", "input-rich-text-md"].includes(name)
110
+ )
111
+ return TiptapEditor
112
+ if (["select", "select-dropdown", "select-multiple"].includes(name)) return Select
113
+ if (name === "datetime") return DateTime
114
+ if (["file", "files", "file-image"].includes(name)) return FileInput
115
+ if (name === "boolean") return BooleanInput
116
+ if (name.includes("many") || name.includes("one") || name.includes("to")) return RelationSelect
117
+ return Input
118
+ })
119
+
120
+ const required = computed(() => {
121
+ const meta = field?.meta ?? {}
122
+ return Boolean(meta?.validation?.required ?? meta?.required ?? false)
123
+ })
124
+
125
+ const safeOptions = computed(() => field?.meta?.options ?? null)
126
+ const defaultValue = computed(() => field?.schema?.default_value ?? null)
127
+ const isMultiple = computed(() => {
128
+ const name = String(field?.meta?.interface ?? "").toLowerCase()
129
+ if (name === "select-multiple") return true
130
+ if (name.includes("many")) return true
131
+ return Boolean(field?.meta?.options?.multiple || false)
132
+ })
133
+
134
+ return {
135
+ internalValue,
136
+ internalAuto,
137
+ label,
138
+ fieldLookup,
139
+ required,
140
+ safeOptions,
141
+ defaultValue,
142
+ isMultiple,
143
+ }
144
+ }
@@ -0,0 +1,32 @@
1
+ import { useNuxtApp } from '#imports'
2
+
3
+ /**
4
+ * Centralized safe wrapper around `$directus.request`.
5
+ * Shows a toast error when the Directus client or `.request` is not available.
6
+ */
7
+ export default function useDirectusRequest() {
8
+ const nuxt = useNuxtApp() as any
9
+
10
+ async function request(config: any) {
11
+ try {
12
+ const client = nuxt?.$directus
13
+ if (!client || typeof client.request !== 'function') {
14
+ try {
15
+ const toast = nuxt?.$toast
16
+ if (toast && typeof toast.error === 'function') toast.error('Directus client unavailable')
17
+ } catch (_) {}
18
+ throw new Error('Directus client.request is not available')
19
+ }
20
+
21
+ return await client.request(config)
22
+ } catch (e) {
23
+ try {
24
+ const toast = nuxt?.$toast
25
+ if (toast && typeof toast.error === 'function') toast.error('Request failed')
26
+ } catch (_) {}
27
+ throw e
28
+ }
29
+ }
30
+
31
+ return { request }
32
+ }
@@ -0,0 +1,9 @@
1
+ export async function useDirectusSchema(collection) {
2
+ const config = useRuntimeConfig()
3
+
4
+ return await $fetch(`${config.public.directus.url}/fields/${collection}`, {
5
+ headers: {
6
+ Authorization: `Bearer ${config.public.directus.auth.token}`
7
+ }
8
+ })
9
+ }
@@ -0,0 +1,17 @@
1
+ export function useLivePreview() {
2
+ return usePreviewMode({
3
+ // Enable preview mode when both preview and token params exist in URL
4
+ shouldEnable: () => {
5
+ const route = useRoute();
6
+ return !!route.query.preview && !!route.query.token;
7
+ },
8
+
9
+ // Store the token from the URL for use in API calls
10
+ getState: (currentState) => {
11
+ const route = useRoute();
12
+ return {
13
+ token: route.query.token || currentState.token,
14
+ };
15
+ },
16
+ });
17
+ }
@@ -0,0 +1,38 @@
1
+ import { apply as applyVisualEditing, setAttr } from '@directus/visual-editing';
2
+ import type { PrimaryKey } from '@directus/types';
3
+
4
+ interface ApplyOptions {
5
+ directusUrl: string;
6
+ elements?: HTMLElement[] | HTMLElement;
7
+ onSaved?: (data: { collection?: string; item?: PrimaryKey | null; payload?: Record<string, any> }) => void;
8
+ customClass?: string;
9
+ }
10
+ export default function useVisualEditing() {
11
+ // Use useState for state that persists across navigation
12
+ const isVisualEditingEnabled = useState('visual-editing-enabled', () => false);
13
+ const route = useRoute();
14
+ const {
15
+ public: { enableVisualEditing, directusUrl },
16
+ } = useRuntimeConfig();
17
+
18
+ // Check query param on composable initialization.
19
+ if (route.query['visual-editing'] === 'true' && enableVisualEditing) {
20
+ isVisualEditingEnabled.value = true;
21
+ } else if (route.query['visual-editing'] === 'false') {
22
+ isVisualEditingEnabled.value = false;
23
+ }
24
+
25
+ const apply = (options: Pick<ApplyOptions, 'elements' | 'onSaved' | 'customClass'>) => {
26
+ if (!isVisualEditingEnabled.value) return;
27
+ applyVisualEditing({
28
+ ...options,
29
+ directusUrl,
30
+ });
31
+ };
32
+
33
+ return {
34
+ isVisualEditingEnabled,
35
+ apply,
36
+ setAttr,
37
+ };
38
+ }
@@ -0,0 +1,15 @@
1
+ import { defineComponent, provide } from 'vue';
2
+ import type { MeeoviDirectusClient } from '../client/createClient';
3
+
4
+ export const DirectusKey = Symbol('DirectusClient');
5
+
6
+ export default defineComponent({
7
+ name: 'DirectusProvider',
8
+ props: {
9
+ client: { type: Object, required: true }
10
+ },
11
+ setup(props, { slots }) {
12
+ provide(DirectusKey, props.client as MeeoviDirectusClient<any>);
13
+ return () => slots.default?.();
14
+ }
15
+ });
@@ -0,0 +1,13 @@
1
+ import { inject } from 'vue';
2
+ import { DirectusKey } from './DirectusProvider';
3
+ import type { MeeoviDirectusClient } from '../client/createClient';
4
+
5
+ export function useVDirectus<Schema>() {
6
+ const client = inject<MeeoviDirectusClient<Schema>>(DirectusKey);
7
+
8
+ if (!client) {
9
+ throw new Error('Directus client not provided. Wrap your app in <DirectusProvider>.');
10
+ }
11
+
12
+ return client;
13
+ }
package/tsconfig.json ADDED
File without changes