Token导航 LogoToken导航TokenDH.com
研究检索需要联网github未标认证来源可访问许可证需确认审计提醒

constructive-meta-forms建设性元形式

Agent Skill

constructive-meta-forms 用于查找、检索和筛选相关信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要根据关键词、任务场景或来源线索快速定位候选结果时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

339

周安装

14

GitHub Stars

公开资料未说明

下载量

111
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:constructive-meta-forms(建设性元形式)
来源仓库:https://github.com/constructive-io/constructive-skills
仓库路径:skills/constructive-meta-forms
安装命令:
npx skills add https://github.com/constructive-io/constructive-skills --skill constructive-meta-forms
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/constructive-io/constructive-skills --skill constructive-meta-forms

简介

利用 _meta 查询动态生成任意表的 CRUD 表单,无需静态字段配置。

  • 自动获取字段类型、必填状态和外键关系,实现零代码生成的表单系统。
  • 适用于快速原型开发或多租户系统的灵活表单需求。
  • 依赖 Constructive GraphQL 端点的 _meta 能力,需确保 API 可用。
  • constructive-meta-forms 属于研究检索类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

Constructive _meta Dynamic Forms

Build fully dynamic CRUD forms for any Constructive-provisioned table — zero static field configuration required. The _meta query built into every Constructive app-public GraphQL endpoint tells you field names, types, required status, FK relationships, and mutation names — all at runtime.

One component. Any table. No codegen needed for forms.


1. What _meta gives you

query GetMeta {
  _meta {
    tables {
      name
      fields { name isNotNull hasDefault type { pgType gqlType isArray } }
      inflection { tableType createInputType patchType filterType orderByType }
      query { all one create update delete }
      primaryKeyConstraints { name fields { name } }
      foreignKeyConstraints { name fields { name } referencedTable referencedFields }
      uniqueConstraints { name fields { name } }
    }
  }
}
  • fields → names, types, nullability, defaults — enough to render any input
  • inflection → exact GraphQL type names for mutations (CreateContactInput, ContactPatch)
  • query → exact mutation/query resolver names (createContact, updateContact, deleteContact)
  • foreignKeyConstraints → which fields are FKs and what table they reference
  • Fetch once with staleTime: Infinity — schema never changes at runtime

2. TypeScript types

// src/types/meta.ts
export type MetaField = {
  name: string;
  isNotNull: boolean;
  hasDefault: boolean;
  type: { pgType: string; gqlType: string; isArray: boolean };
};

export type MetaTable = {
  name: string;
  fields: MetaField[];
  inflection: {
    tableType: string;
    createInputType: string;
    patchType: string | null;
    filterType: string | null;
    orderByType: string;
  };
  query: {
    all: string;         // e.g. "contacts"
    one: string | null;  // ⚠️ may be a non-existent root field — see §3 bug note
    create: string | null;
    update: string | null;
    delete: string | null;
  };
  primaryKeyConstraints: Array<{ name: string; fields: { name: string }[] }>;
  foreignKeyConstraints: Array<{
    name: string;
    fields: { name: string }[];
    referencedTable: string;
    referencedFields: string[];
  }>;
  uniqueConstraints: Array<{ name: string; fields: { name: string }[] }>;
};

3. ⚠️ Platform bug: query.one returns a non-existent root field

_meta.query.one returns the singular name (e.g. "contact") but the Constructive GraphQL root only exposes plural queries (e.g. contacts). Using query.one as the root field will fail.

Fix — always use query.all + condition: {id: $id}:

function buildFetchQuery(table: MetaTable): string {
  const fieldNames = table.fields.map((f) => f.name).join('\n    ');
  // Use query.all with a condition filter + read nodes[0]
  // DO NOT use query.one — it returns a non-existent root field name
  return `
    query DynamicFetch($id: UUID!) {
      ${table.query.all}(condition: { id: $id }) {
        nodes { ${fieldNames} }
      }
    }
  `;
}

// Read the result:
const result = data[table.query.all].nodes[0] as Record<string, unknown> | undefined;

4. useMeta / useTableMeta hooks

// src/lib/meta/use-meta.ts
'use client';
import { useQuery } from '@tanstack/react-query';
import { CRM_ENDPOINT } from '@/components/crm/crm-provider';
import { TokenManager } from '@/lib/auth/token-manager';
import type { MetaTable } from '@/types/meta';

const META_QUERY = `query GetMeta {
  _meta {
    tables {
      name
      fields { name isNotNull hasDefault type { pgType gqlType isArray } }
      inflection { tableType createInputType patchType filterType orderByType }
      query { all one create update delete }
      primaryKeyConstraints { name fields { name } }
      foreignKeyConstraints { name fields { name } referencedTable referencedFields }
      uniqueConstraints { name fields { name } }
    }
  }
}`;

async function fetchMeta(): Promise<{ _meta: { tables: MetaTable[] } }> {
  const { token } = TokenManager.getToken('schema-builder');
  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  };
  if (token) headers['Authorization'] = `Bearer ${token.accessToken}`;
  const res = await fetch(CRM_ENDPOINT, {
    method: 'POST', headers,
    body: JSON.stringify({ query: META_QUERY }),
  });
  if (!res.ok) throw new Error(`_meta fetch failed: ${res.status}`);
  const json = await res.json();
  if (json.errors?.length) throw new Error(json.errors[0].message ?? '_meta error');
  return json.data;
}

export function useMeta() {
  return useQuery({ queryKey: ['_meta'], queryFn: fetchMeta, staleTime: Infinity });
}

export function useTableMeta(tableName: string): MetaTable | null {
  const { data } = useMeta();
  return data?._meta.tables.find((t) => t.name === tableName) ?? null;
}

5. Field renderer utilities

// src/lib/meta/field-renderer.ts
import type { MetaField } from '@/types/meta';

/** System fields — always skip in forms (auto-managed by Constructive) */
export const SYSTEM_FIELDS = new Set([
  'id', 'entityId', 'createdAt', 'updatedAt',
  'created_at', 'updated_at', 'entity_id',
]);

export type FieldInputType =
  | 'text' | 'textarea' | 'number' | 'boolean'
  | 'date' | 'datetime' | 'uuid' | 'json' | 'select' | 'hidden';

const TEXTAREA_HINTS = ['bio', 'description', 'notes', 'body', 'content', 'summary', 'details'];

export function getInputType(field: MetaField, isForeignKey: boolean): FieldInputType {
  if (SYSTEM_FIELDS.has(field.name)) return 'hidden';
  if (isForeignKey) return 'select';
  const pg = field.type.pgType.toLowerCase();
  switch (pg) {
    case 'text': case 'varchar': case 'citext':
      return TEXTAREA_HINTS.some((h) => field.name.toLowerCase().includes(h)) ? 'textarea' : 'text';
    case 'int2': case 'int4': case 'int8':
    case 'float4': case 'float8': case 'numeric': return 'number';
    case 'bool': case 'boolean': return 'boolean';
    case 'date': return 'date';
    case 'timestamp': case 'timestamptz': return 'datetime';
    case 'uuid': return 'uuid';
    case 'json': case 'jsonb': return 'json';
    default: return 'text';
  }
}

/**
 * A field is required if it's NOT NULL AND has no server-side default.
 * hasDefault=true = Constructive auto-generates the value (ids, timestamps, etc.) — never require in forms.
 */
export function isRequiredField(field: MetaField): boolean {
  return field.isNotNull && !field.hasDefault;
}

/** camelCase → "Title Case" label */
export function toLabel(fieldName: string): string {
  return fieldName.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()).trim();
}

Required field rule

isNotNullhasDefaultIn form
truefalseRequired input
truetrueSkip in create (id, timestamps), optional in edit
falseanythingOptional input

6. DynamicField component

Handles all pgTypes automatically. Add locked + lockedLabel for pre-filled FK context (see §8).

// src/components/crm/dynamic-field.tsx
'use client';
import { Field } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { getInputType, SYSTEM_FIELDS, toLabel } from '@/lib/meta/field-renderer';
import type { MetaField } from '@/types/meta';
import { Lock } from 'lucide-react';

type DynamicFieldProps = {
  field: MetaField;
  value: unknown;
  onChange: (value: unknown) => void;
  isForeignKey?: boolean;
  /** Pre-set from context — visible but not editable */
  locked?: boolean;
  /** Human-readable label for locked field (e.g. "Kristopher Floyd" instead of a UUID) */
  lockedLabel?: string;
  error?: string;
};

export function DynamicField({
  field, value, onChange,
  isForeignKey = false, locked = false, lockedLabel, error,
}: DynamicFieldProps) {
  if (SYSTEM_FIELDS.has(field.name)) return null;

  const inputType = getInputType(field, isForeignKey);
  const label = toLabel(field.name);
  const required = field.isNotNull && !field.hasDefault;

  // ── Locked: visible, disabled, not editable ──
  if (locked) {
    const displayValue = lockedLabel ?? (typeof value === 'string' ? value : String(value ?? ''));
    return (
      <Field label={label} required={false}>
        <div className="relative">
          <Input
            value={displayValue}
            readOnly disabled
            className="bg-muted/40 pr-8 text-muted-foreground cursor-default"
          />
          <Lock className="absolute right-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/60" />
        </div>
        {lockedLabel && (
          <p className="mt-1 text-xs text-muted-foreground font-mono">{String(value)}</p>
        )}
      </Field>
    );
  }

  if (inputType === 'hidden') return null;

  if (inputType === 'boolean') {
    return (
      <div className="flex items-center gap-3 py-1">
        <Switch id={field.name} checked={(value as boolean) ?? false} onCheckedChange={onChange} />
        <Label htmlFor={field.name} className="cursor-pointer">{label}</Label>
        {error && <p className="text-destructive text-sm">{error}</p>}
      </div>
    );
  }

  if (inputType === 'textarea') {
    return (
      <Field label={label} required={required} error={error}>
        <Textarea value={(value as string) ?? ''} onChange={(e) => onChange(e.target.value)} rows={4} />
      </Field>
    );
  }

  if (inputType === 'json') {
    return (
      <Field label={label} required={required} error={error} description="JSON value">
        <Textarea
          value={typeof value === 'string' ? value : JSON.stringify(value ?? null, null, 2)}
          onChange={(e) => { try { onChange(JSON.parse(e.target.value)); } catch { onChange(e.target.value); } }}
          rows={6} className="font-mono text-xs"
        />
      </Field>
    );
  }

  if (inputType === 'number') {
    return (
      <Field label={label} required={required} error={error}>
        <Input type="number" value={(value as number) ?? ''}
          onChange={(e) => onChange(e.target.value === '' ? undefined : Number(e.target.value))} />
      </Field>
    );
  }

  if (inputType === 'date') {
    return (
      <Field label={label} required={required} error={error}>
        <Input type="date" value={(value as string) ?? ''} onChange={(e) => onChange(e.target.value)} />
      </Field>
    );
  }

  if (inputType === 'datetime') {
    return (
      <Field label={label} required={required} error={error}>
        <Input type="datetime-local" value={(value as string) ?? ''} onChange={(e) => onChange(e.target.value)} />
      </Field>
    );
  }

  if (inputType === 'uuid') {
    return (
      <Field label={label} required={required} error={error}>
        <Input value={(value as string) ?? ''} onChange={(e) => onChange(e.target.value)}
          placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" className="font-mono" />
      </Field>
    );
  }

  if (inputType === 'select') {
    // FK field — raw UUID input until EntitySearch is built
    return (
      <Field label={label} required={required} error={error} description="Foreign key — paste UUID">
        <Input value={(value as string) ?? ''} onChange={(e) => onChange(e.target.value)}
          placeholder={`${label} ID…`} className="font-mono text-sm" />
      </Field>
    );
  }

  return (
    <Field label={label} required={required} error={error}>
      <Input value={(value as string) ?? ''} onChange={(e) => onChange(e.target.value)} />
    </Field>
  );
}

7. DynamicFormCard — full implementation

// src/components/crm/dynamic-form-card.tsx
'use client';
import { useMemo, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { CardComponent } from '@/components/ui/stack';
import { useCardReady } from '@/components/ui/stack';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { showSuccessToast, showErrorToast } from '@/components/ui/toast';
import { SYSTEM_FIELDS, isRequiredField } from '@/lib/meta/field-renderer';
import { useTableMeta } from '@/lib/meta/use-meta';
import { DynamicField } from './dynamic-field';
import { Loader2 } from 'lucide-react';
import { CRM_ENDPOINT } from '@/components/crm/crm-provider';
import { TokenManager } from '@/lib/auth/token-manager';
import type { MetaTable } from '@/types/meta';

export type DynamicFormCardProps = {
  /** Constructive table type name, e.g. 'Contact', 'Note', 'Deal' */
  tableName: string;
  /** Existing record ID — omit for create mode */
  recordId?: string;
  /**
   * Pre-set field values from context (typically FK fields).
   * e.g. { contactId: "uuid" } when adding a Note from a Contact page.
   * These fields are rendered as visible-but-locked (disabled, 🔒 icon).
   */
  defaultValues?: Record<string, unknown>;
  /**
   * Human-readable display labels for locked fields.
   * e.g. { contactId: "Kristopher Floyd" } → shows name, UUID as helper text.
   */
  defaultValueLabels?: Record<string, string>;
  /** Called after successful save or delete */
  onSuccess?: () => void;
};

async function crmRequest(query: string, variables?: Record<string, unknown>) {
  const { token } = TokenManager.getToken('schema-builder');
  const headers: Record<string, string> = {
    'Content-Type': 'application/json', Accept: 'application/json',
  };
  if (token) headers['Authorization'] = `Bearer ${token.accessToken}`;
  const res = await fetch(CRM_ENDPOINT, {
    method: 'POST', headers, body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) throw new Error(`GraphQL error: ${res.status}`);
  const json = await res.json();
  if (json.errors?.length) throw new Error(json.errors[0].message);
  return json.data;
}

function buildFetchQuery(table: MetaTable): string {
  const fields = table.fields.map((f) => f.name).join('\n      ');
  // Use query.all + condition — NOT query.one (platform bug: query.one is non-existent root field)
  return `
    query DynamicFetch($id: UUID!) {
      ${table.query.all}(condition: { id: $id }) {
        nodes { ${fields} }
      }
    }
  `;
}

export const DynamicFormCard: CardComponent<DynamicFormCardProps> = ({
  tableName, recordId, defaultValues, defaultValueLabels, onSuccess, card,
}) => {
  const isEditMode = !!recordId;
  const { isReady } = useCardReady();
  const tableMeta = useTableMeta(tableName);
  const queryClient = useQueryClient();

  // Seed formValues with defaultValues so locked fields are in place immediately
  const [formValues, setFormValues] = useState<Record<string, unknown>>(defaultValues ?? {});
  const [initialized, setInitialized] = useState(false);
  const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
  const [isSaving, setIsSaving] = useState(false);

  const fkFields = useMemo(
    () => new Set(tableMeta?.foreignKeyConstraints.flatMap((fk) => fk.fields.map((f) => f.name)) ?? []),
    [tableMeta],
  );

  const editableFields = useMemo(
    () => tableMeta?.fields.filter((f) => !SYSTEM_FIELDS.has(f.name)) ?? [],
    [tableMeta],
  );

  // Locked = pre-set from defaultValues, cannot be changed by user
  const lockedFields = useMemo(
    () => new Set(Object.keys(defaultValues ?? {})),
    [defaultValues],
  );

  const { data: existingData, isLoading: isLoadingRecord } = useQuery({
    queryKey: ['dynamic-record', tableName, recordId],
    queryFn: async () => {
      const query = buildFetchQuery(tableMeta!);
      const data = await crmRequest(query, { id: recordId });
      return (data[tableMeta!.query.all]?.nodes?.[0] ?? null) as Record<string, unknown> | null;
    },
    enabled: isReady && isEditMode && !!tableMeta,
    staleTime: 0,
  });

  // Initialize form from existing record — locked fields take precedence
  if (existingData && !initialized) {
    const initial: Record<string, unknown> = { ...(defaultValues ?? {}) };
    for (const field of editableFields) {
      if (!lockedFields.has(field.name) && existingData[field.name] !== undefined) {
        initial[field.name] = existingData[field.name];
      }
    }
    setFormValues(initial);
    setInitialized(true);
  }

  const setFieldValue = (name: string, value: unknown) => {
    setFormValues((prev) => ({ ...prev, [name]: value }));
    setFieldErrors((prev) => { const next = { ...prev }; delete next[name]; return next; });
  };

  // Validate — skip locked fields (always satisfied by caller)
  const validate = (): boolean => {
    const errors: Record<string, string> = {};
    for (const field of editableFields) {
      if (lockedFields.has(field.name)) continue;
      if (isRequiredField(field)) {
        const val = formValues[field.name];
        if (val === undefined || val === null || val === '') {
          errors[field.name] = `${field.name} is required`;
        }
      }
    }
    setFieldErrors(errors);
    return Object.keys(errors).length === 0;
  };

  const handleSave = async () => {
    if (!tableMeta || !validate()) return;
    setIsSaving(true);
    try {
      const input: Record<string, unknown> = {};
      for (const field of editableFields) {
        const val = formValues[field.name];
        if (val !== undefined && val !== '') input[field.name] = val;
      }

      if (isEditMode) {
        const mutation = `
          mutation DynamicUpdate($id: UUID!, $patch: ${tableMeta.inflection.patchType}!) {
            ${tableMeta.query.update}(input: { id: $id, patch: $patch }) { clientMutationId }
          }`;
        await crmRequest(mutation, { id: recordId, patch: input });
      } else {
        const mutation = `
          mutation DynamicCreate($input: ${tableMeta.inflection.createInputType}!) {
            ${tableMeta.query.create}(input: { input: $input }) { clientMutationId }
          }`;
        await crmRequest(mutation, { input });
      }

      await queryClient.invalidateQueries({ queryKey: [tableMeta.query.all] });
      if (isEditMode) await queryClient.invalidateQueries({ queryKey: ['dynamic-record', tableName, recordId] });
      showSuccessToast({ message: isEditMode ? `${tableName} updated` : `${tableName} created` });
      onSuccess?.();
      card.close();
    } catch (err) {
      showErrorToast({
        message: `Failed to ${isEditMode ? 'update' : 'create'} ${tableName}`,
        description: err instanceof Error ? err.message : 'Unknown error',
      });
    } finally {
      setIsSaving(false);
    }
  };

  const handleDelete = () => {
    if (!tableMeta || !recordId) return;
    card.push({
      id: `confirm-delete-${recordId}`,
      title: `Delete ${tableName}?`,
      description: 'This cannot be undone.',
      Component: ConfirmDeleteCard,
      props: {
        tableName, recordId,
        deleteMutation: tableMeta.query.delete!,
        tableType: tableMeta.inflection.tableType,
        listQueryKey: tableMeta.query.all,
        onSuccess: () => { onSuccess?.(); card.close(); },
      },
      width: 400,
    });
  };

  if (!tableMeta || (isEditMode && isLoadingRecord && !initialized)) {
    return (
      <div className="flex h-full flex-col p-4 space-y-4">
        {[1, 2, 3, 4].map((i) => (
          <div key={i} className="space-y-2">
            <Skeleton className="h-4 w-28" />
            <Skeleton className="h-9 w-full" />
          </div>
        ))}
      </div>
    );
  }

  return (
    <div className="flex h-full flex-col">
      <div className="flex-1 space-y-4 overflow-y-auto p-4">
        {editableFields.map((field) => (
          <DynamicField
            key={field.name}
            field={field}
            value={formValues[field.name]}
            onChange={(val) => setFieldValue(field.name, val)}
            isForeignKey={fkFields.has(field.name)}
            locked={lockedFields.has(field.name)}
            lockedLabel={defaultValueLabels?.[field.name]}
            error={fieldErrors[field.name]}
          />
        ))}
        {editableFields.length === 0 && (
          <p className="text-muted-foreground py-8 text-center text-sm">No editable fields.</p>
        )}
      </div>
      <div className="flex items-center justify-between border-t px-4 py-3">
        {isEditMode && tableMeta.query.delete ? (
          <Button variant="destructive" size="sm" onClick={handleDelete} disabled={isSaving}>Delete</Button>
        ) : <div />}
        <div className="flex gap-2">
          <Button variant="outline" onClick={() => card.close()} disabled={isSaving}>Cancel</Button>
          <Button onClick={handleSave} disabled={isSaving}>
            {isSaving
              ? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />Saving…</>
              : isEditMode ? 'Save Changes' : `Create ${tableName}`}
          </Button>
        </div>
      </div>
    </div>
  );
};

ConfirmDeleteCard (add in same file)

type ConfirmDeleteCardProps = {
  tableName: string; recordId: string; deleteMutation: string;
  tableType: string; listQueryKey: string; onSuccess?: () => void;
};

const ConfirmDeleteCard: CardComponent<ConfirmDeleteCardProps> = ({
  tableName, recordId, deleteMutation, tableType, listQueryKey, onSuccess, card,
}) => {
  const queryClient = useQueryClient();
  const [isDeleting, setIsDeleting] = useState(false);

  const handleConfirm = async () => {
    setIsDeleting(true);
    try {
      const mutation = `mutation DynamicDelete($id: UUID!) {
        ${deleteMutation}(input: { id: $id }) { deleted${tableType}Id }
      }`;
      await crmRequest(mutation, { id: recordId });
      await queryClient.invalidateQueries({ queryKey: [listQueryKey] });
      showSuccessToast({ message: `${tableName} deleted` });
      onSuccess?.(); card.close();
    } catch (err) {
      showErrorToast({
        message: `Failed to delete ${tableName}`,
        description: err instanceof Error ? err.message : 'Unknown error',
      });
      setIsDeleting(false);
    }
  };

  return (
    <div className="flex h-full flex-col">
      <div className="flex-1 p-4">
        <p className="text-muted-foreground text-sm">
          Are you sure you want to delete this {tableName.toLowerCase()}? This cannot be undone.
        </p>
      </div>
      <div className="flex justify-end gap-2 border-t px-4 py-3">
        <Button variant="outline" onClick={() => card.close()} disabled={isDeleting}>Cancel</Button>
        <Button variant="destructive" onClick={handleConfirm} disabled={isDeleting}>
          {isDeleting ? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />Deleting…</> : `Delete ${tableName}`}
        </Button>
      </div>
    </div>
  );
};

8. Locked FK pre-fill — related records from context

When opening a form from a parent record page (e.g. adding a Note from a Contact detail page), pass defaultValues to pre-set and lock the FK field. The user sees it but cannot change it.

// On Kristopher Floyd's contact page:
const contactFullName = `${contact.firstName} ${contact.lastName}`;

// ── Create a new note (+ Add Note button) ──
stack.push({
  id: `add-note-${contactId}`,
  title: 'Add Note',
  description: `New note for ${contactFullName}`,
  Component: DynamicFormCard,
  props: {
    tableName: 'Note',
    defaultValues: { contactId },              // pre-set FK, locked
    defaultValueLabels: { contactId: contactFullName }, // show name, not UUID
    onSuccess: () => queryClient.invalidateQueries({ queryKey: noteKeys.lists() }),
  },
  width: 480,
});

// ── Edit an existing note (click note row) ──
stack.push({
  id: `edit-note-${noteId}`,
  title: 'Edit Note',
  Component: DynamicFormCard,
  props: {
    tableName: 'Note',
    recordId: noteId,
    defaultValues: { contactId },              // locked even in edit — can't reassign owner
    defaultValueLabels: { contactId: contactFullName },
    onSuccess: () => queryClient.invalidateQueries({ queryKey: noteKeys.lists() }),
  },
  width: 480,
});

How it renders:

  • Contact Id field → disabled input showing "Kristopher Floyd" + 🔒 icon
  • UUID shown as small helper text below
  • Field cannot be changed by user
  • Value is included in the save mutation automatically
  • Validation skips locked fields (they're always satisfied)

Generic rule: defaultValues works for any FK on any table. The _meta FK constraint map tells you which fields are FKs — you don't need to hardcode anything.


9. Usage patterns

import { DynamicFormCard } from '@/components/crm/dynamic-form-card';

// ── Create any record ──
stack.push({ id: 'new-contact', title: 'New Contact',
  Component: DynamicFormCard, props: { tableName: 'Contact' }, width: 480 });

// ── Edit any record ──
stack.push({ id: `edit-${id}`, title: 'Edit Contact',
  Component: DynamicFormCard, props: { tableName: 'Contact', recordId: id }, width: 480 });

// ── Related record (O2M) from parent page ──
stack.push({ id: `add-note-${contactId}`, title: 'Add Note',
  Component: DynamicFormCard,
  props: { tableName: 'Note', defaultValues: { contactId }, defaultValueLabels: { contactId: name } },
  width: 480 });

// ── Any table, same API ──
stack.push({ id: 'new-deal', title: 'New Deal',
  Component: DynamicFormCard, props: { tableName: 'Deal' }, width: 480 });

10. pgType → input type reference

pgTypeInputNotes
text, varchar, citext<Input><Textarea> if name contains bio/description/notes/body
int2/4/8, float4/8, numeric<Input type="number">
bool, boolean<Switch>
date<Input type="date">
timestamp, timestamptz<Input type="datetime-local">
uuid (FK)Locked or UUID inputUse defaultValues to lock from context; future: <EntitySearch>
uuid (non-FK)<Input> monoRare — raw UUID
json, jsonb<Textarea> monoJSON.parse / stringify

11. Future extensions

FeatureHow
EntitySearch for FK fieldsReplace select case in DynamicField with an <EntitySearch tableName={fk.referencedTable}> component that fetches + autocompletes
Array fieldsHandle isArray: true in MetaField — render <TagInput> for text[]
Enum fieldsQuery __schema for enum values — render <Select>
PackageExtract DynamicFormCard, DynamicField, useMeta, field-renderer into @constructive/meta-forms npm package so any Constructive-backed app gets this for free

12. Troubleshooting

IssueFix
Single-record fetch fails / field emptyUse query.all + condition: {id: $id} and read nodes[0]query.one returns a non-existent root field (platform bug)
_meta returns empty tablesCheck auth headers — _meta requires an authenticated request
Mutation fails with GraphQL type errorVerify inflection.patchType / createInputType match your schema version
Form shows no editable fieldsAll fields in SYSTEM_FIELDS — check provisioned columns
Required validation on system fieldsBug — verify SYSTEM_FIELDS set covers all auto-managed field names
Edit form is empty on openCheck useCardReady() gate — data fetches only after card animation completes
FK shows UUID instead of nameUse defaultValueLabels prop, or build EntitySearch (future work)
hasDefault=true field marked requiredBug in isRequiredField — must check !hasDefault

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

35.61%
按下载量换算40

Claude

27.98%
按下载量换算31

Cursor

19.31%
按下载量换算21

Gemini CLI

8.76%
按下载量换算10

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills