@m3r/sdk 0.5.0

ctx.ext

Tabela, tela, menu, relatório e records da extensão. JSONB ou tabela física FAX####. O ERP pinta.

ctx.ext não mistura com ctx.m3r.sales. Você declara schema e o host /apps/extensions/{code} renderiza DataTable, Modal e PageHeader.

O index.ts não declara schema. Ele chama registerVendas(ctx). Cada ensure / define vive na pasta do domínio. Árvore e ordem: Padrões.

src/vendas/tabelas/lote.ts
import {
  SdkColumnType, SdkFormBind, SdkMenuParent,
  SdkNavTarget, SdkReportFilter, SdkScreenKind, SdkTableStorage
} from "@m3r/sdk";

await ctx.ext.tables.ensure({ /* … */ });

m3r.json precisa listar os mesmos tables[], screens[], nav[], reports[] de todos os arquivos em src/**. Validate varre o fonte, não só o index.

Limites (0.5.0)

RecursoTetoSe estourar
Tabelas / extensão / tenant20SdkValidationError
Colunas / tabela30idem
Bytes / linha64 KBAPI recusa
Itens de menu20SdkValidationError
Relatórios10idem

tables.ensure

CampoTipoRegra
namestringSlug [a-z][a-z0-9_]{1,47}. Id lógico. Sem SQL identifier do Core.
labelstringTítulo.
columns[].namestringMesmo slug. Reservados abaixo.
columns[].typeSdkColumnTypestring, text, number, boolean, date, enum.
columns[].optionsstring[]Obrigatório se enum.
columns[].requiredbooleanDefault false.
uniqueKeysstring[][]Opcional. Physical = UNIQUE no Postgres (WHERE delete_date IS NULL). Document = o Core compara as linhas JSONB vivas.
storageSdkTableStorageDocument (padrão, JSONB em FADEV07) ou Physical (tabela FAX#### criada pelo Core).
physicalNameFAX0001–FAX9999Opcional. Se omitir, sequence fadev_physical_table_seq — pula código já ocupado — e devolve no retorno. Não muda depois.

O name é o id lógico. O nome físico não é um identificador SQL seu: ou o Core atribui FAX0001…FAX9999, ou você pede um physicalName livre nesse formato. Depois do primeiro ensure, não muda. Toda tabela física nasce com id, company_id, extension_id, created_by, created_at, updated_at, delete_date. Esses nomes são reservados — não declare.

Evolução: só adicionar coluna. Sem drop, sem rename, sem mudar tipo, sem trocar storage. Sem FK para tabela do Core (FAT003, financeiro, …). Isolamento do dado: companyId + extensionId. FADEV02 (development / staging / production) não cria banco separado.

Não pode: json, uuid, FK para FAT003, SQL, escolher o código FA*.

tables.ensure
const lote = await ctx.ext.tables.ensure({
  name: "demo_lote",
  label: "Lote físico",
  storage: SdkTableStorage.Physical,
  // physicalName: "FAX0042", // opcional — senão o Core devolve FAX0001…
  columns: [
    { name: "codigo", type: SdkColumnType.String, required: true, label: "Código" },
    { name: "qtd", type: SdkColumnType.Number, required: true, label: "Qtd" }
  ],
  uniqueKeys: [["codigo"]]
});
ctx.logger.info("físico", { physicalName: lote.physicalName });

screens.define

CampoTipoRegra
codestringSlug [a-z][a-z0-9_.-]{1,62}. Vira URL /apps/extensions/{code}.
kindSdkScreenKindPage ou Modal.
tablestringJá passou por ensure.
list.columnsstring[]Colunas da DataTable.
form.fieldsstring ou objetobind só SdkFormBind.
actions{ id, label, opens? }[]Opcional.

SdkFormBind: document.id, document.code, document.status, document.observation, document.customerName, document.customerId, document.deliveryDate, document.orderDocType.

Qualquer outro path: assertFormBind → SdkValidationError.

Modal com bind
await ctx.ext.screens.define({
  code: "demo.auditoria.novo",
  title: "Nova auditoria",
  kind: SdkScreenKind.Modal,
  table: "demo_auditoria",
  form: {
    fields: [
      { name: "pedido_codigo", bind: SdkFormBind.DocumentCode, readonly: true },
      { name: "status", default: "aberto" }
    ]
  }
});

Depois do mount: IP.EXT.SCREEN.ON_LOAD. Dá para ui.setField / ui.disableField / ui.hideField. Não nega o load.

actions[].opens aceita código de tela ou prefixo explícito: openScreen:, openModal:, openReport:. Sem prefixo, o host trata como tela. Relatório vai para /apps/extensions/reports/{code}. Se a origem for venda, a URL da tela leva documentId / documentCode e o ON_LOAD recebe o documento.

records

CRUD na sua tabela. Isolado por empresa + extensão.

MétodoEntrada
find(table, { limit?, cursor? })página das linhas vivas
get(table, id)uma linha (id, tableName, data, createdBy, createdAt, updatedAt)
create(table, data)objeto das colunas; recusa se uniqueKeys colidir
update(table, id, data)patch do data; mesma checagem de unique
remove(table, id)soft delete — delete_date preenchido. A linha some do find e da tela. Unique volta a aceitar o mesmo valor.

O CRUD da tela no ERP usa as mesmas regras. Dá para gravar no AFTER_SAVE ou num ctx.events.on — a sample 0.6.1 faz isso na auditoria. Não leia pedido de venda por aqui — use ctx.m3r.

Extensão desativada no tenant recusa tela, relatório e records.

Sem URL livre. Alvo = tela ou relatório seu.

CampoTipoRegra
id / labelstringEm nav[]. Mesmo slug de tela ([a-z][a-z0-9_.-]{1,62}).
iconstring?MDI (mdi:…).
target.typeSdkNavTargetScreen ou Report.
target.codestringCódigo já definido.
placement.parentSdkMenuParentLista fechada — ver Padrões.
placement.sectionSdkNavSectionSó Extensions. Ou parent, nunca os dois.
nav.define
await ctx.ext.nav.define({
  id: "demo.auditoria",
  label: "Auditoria demo",
  icon: "mdi:clipboard-text-outline",
  target: { type: SdkNavTarget.Screen, code: "demo.auditoria" },
  placement: { parent: SdkMenuParent.Vendas }
});

parent: "Vendas" (errado) não compile. É SdkMenuParent.Vendas → "vendas".

reports.define

Fonte: só tabela da extensão. PDF = gerador tabular do Core (farmus-reports). Sem Python na extensão.

CampoTipoRegra
code / titlestringHost /apps/extensions/reports/{code}.
tablestringSua tabela.
columns{ key, label }[]key = coluna.
filtersstring ou { type, column?, label? }DateRange (coluna date ou createdAt), Enum, Text.
export—PDF e Excel do mesmo recorte. Sem script Python.
reports.define
await ctx.ext.reports.define({
  code: "demo.auditoria.periodo",
  title: "Auditorias do período",
  table: "demo_auditoria",
  columns: [
    { key: "pedido_codigo", label: "Pedido" },
    { key: "status", label: "Status" }
  ],
  filters: [
    SdkReportFilter.DateRange,
    { type: SdkReportFilter.Enum, column: "status" },
    { type: SdkReportFilter.Text, column: "pedido_codigo" }
  ]
});

Pode / não pode

Pode

  • Persistência isolada para dado da extensão.
  • Menu no grupo Vendas e no grupo Extensões (dois define).
  • PDF e Excel do que está na tabela, com filtro na coluna declarada.

Não pode

  • SQL em FAT003 / financeiro.
  • React, iframe, coluna json.
  • Relatório com query no Core.
  • placement.parent: "foo".