Skip to content

Commit c3e8851

Browse files
committed
no-mistakes(document): Fixed stale documentation: README.md and metaobjects-authoring/SKILL.md updated to reflect report list hook generation; all other docs accurate.
1 parent afaa196 commit c3e8851

4 files changed

Lines changed: 249 additions & 2 deletions

File tree

‎agent-context/skills/metaobjects-authoring/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ no `source.*` is checked at load and generates nothing.
752752
A report `@from` a TPH subtype is refused when its view is derived (the subtype shares its
753753
base's table): declare it `@from` the base with an `@filter` on the discriminator field.
754754

755-
What does not exist: no client hook, grid or form for a report yet, no way to narrow which
755+
What does not exist: no grid or form for a report (TypeScript generates a list hook; no other port has a client tier), no way to narrow which
756756
derived fields are filterable, no `measure.derived`
757757
(arithmetic between measures beyond `measure.ratio`), no query-time choice of dimensions or
758758
measures (a report is a fixed, compiled combination), and no time-zone vocabulary (grains and

‎docs/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ this tree is documentation, not the source of truth.
6666
| Build on a metadata model another repository publishes (`dependencies`, `meta deps sync`, overlay/extend across the boundary) | [`features/metadata-dependencies.md`](features/metadata-dependencies.md) |
6767
| Adopt a design MetaObjects already ships — users/groups/roles, an LLM trace envelope — instead of authoring it (`libraries`, `meta eject <library>`) | [`features/libraries.md`](features/libraries.md) |
6868
| Record what the system is supposed to do, and stop agents reviving retired features | [`features/requirements.md`](features/requirements.md) |
69-
| Declare what a dashboard groups by and counts (`dimension`, `measure`, `segment`, `object.report`; load-time checked; a view-backed report becomes a SQL view served by a generated read-only list route; no client hook yet) | [`features/reporting.md`](features/reporting.md) |
69+
| Declare what a dashboard groups by and counts (`dimension`, `measure`, `segment`, `object.report`; load-time checked; a view-backed report becomes a SQL view served by a generated read-only list route and a TypeScript list hook) | [`features/reporting.md`](features/reporting.md) |
7070
| Wire prompt construction (FR-004) | [`features/templates-and-payloads.md`](features/templates-and-payloads.md) |
7171
| Share a metadata shape across multiple instances (abstracts, `extends:`) | [`features/abstracts-and-inheritance.md`](features/abstracts-and-inheritance.md) |
7272
| Add a custom metamodel subtype or attribute to a downstream project | [`features/extending-with-providers.md`](features/extending-with-providers.md) + [`recipes/extending-metaobjects-with-providers.md`](recipes/extending-metaobjects-with-providers.md) |
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// @generated by @metaobjectsdev/codegen-ts — DO NOT EDIT.
2+
// Source metadata: Article (Article)
3+
// Extend in your own module (e.g. Article.extra.ts) — nothing imports it for you.
4+
import { eq } from "drizzle-orm";
5+
import type { z } from "zod";
6+
7+
import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
8+
type Db = BaseSQLiteDatabase<
9+
"sync" | "async",
10+
unknown,
11+
Record<string, unknown>
12+
>;
13+
14+
import {
15+
type Article,
16+
ArticleInsertPreservingSchema,
17+
ArticleInsertSchema,
18+
type ArticlePatch,
19+
articles,
20+
ArticleUpdateSchema,
21+
} from "./Article";
22+
export async function findArticleById(
23+
db: Db,
24+
id: number,
25+
): Promise<Article | null> {
26+
const [article] = await db
27+
.select()
28+
.from(articles)
29+
.where(eq(articles.id, id))
30+
.limit(1);
31+
return article ?? null;
32+
}
33+
export async function listArticles(
34+
db: Db,
35+
opts?: { limit?: number; offset?: number },
36+
): Promise<Article[]> {
37+
let q = db.select().from(articles).$dynamic();
38+
if (opts?.limit !== undefined) {
39+
q = q.limit(opts.limit);
40+
}
41+
if (opts?.offset !== undefined) {
42+
q = q.offset(opts.offset);
43+
}
44+
return q;
45+
}
46+
export async function createArticle(
47+
db: Db,
48+
data: z.input<typeof ArticleInsertSchema>,
49+
): Promise<Article> {
50+
const validated = ArticleInsertSchema.parse(data);
51+
const [article] = await db.insert(articles).values(validated).returning();
52+
return article!;
53+
}
54+
export async function insertPreservingArticle(
55+
db: Db,
56+
data: z.input<typeof ArticleInsertPreservingSchema>,
57+
): Promise<Article> {
58+
const validated = ArticleInsertPreservingSchema.parse(data);
59+
const [article] = await db.insert(articles).values(validated).returning();
60+
return article!;
61+
}
62+
export async function updateArticle(
63+
db: Db,
64+
id: number,
65+
patch: ArticlePatch,
66+
): Promise<Article | null> {
67+
const validated = ArticleUpdateSchema.parse(patch);
68+
// PATCH-5: an empty patch is a no-op — return the current row rather than let
69+
// Drizzle throw on an empty SET clause.
70+
if (Object.keys(validated).length === 0) {
71+
return findArticleById(db, id);
72+
}
73+
const [article] = await db
74+
.update(articles)
75+
.set(validated)
76+
.where(eq(articles.id, id))
77+
.returning();
78+
return article ?? null;
79+
}
80+
export async function deleteArticleById(db: Db, id: number): Promise<boolean> {
81+
// Use .returning() unconditionally — supported on SQLite ≥3.35 (covers D1, libsql/Turso)
82+
// and Postgres. Result is an array of deleted rows; presence implies success.
83+
const deleted = await db
84+
.delete(articles)
85+
.where(eq(articles.id, id))
86+
.returning();
87+
return deleted.length > 0;
88+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// @generated by @metaobjectsdev/codegen-ts — hand edits are kept by a three-way merge on regeneration; meta verify --codegen checks only the generated parts.
2+
// Source metadata: Article (Article)
3+
// Extend in your own module (e.g. Article.extra.ts) — nothing imports it for you.
4+
import type { InferInsertModel, InferSelectModel } from "drizzle-orm";
5+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
6+
import { z } from "zod";
7+
8+
export const articles = sqliteTable("articles", {
9+
id: integer("id").primaryKey({ autoIncrement: true }),
10+
title: text("title").notNull(),
11+
createdAt: text("created_at").$defaultFn(() => new Date().toISOString()),
12+
updatedAt: text("updated_at").$defaultFn(() => new Date().toISOString()),
13+
});
14+
export type Article = InferSelectModel<typeof articles>;
15+
export type ArticleInsert = InferInsertModel<typeof articles>;
16+
export type ArticleUpdate = Partial<ArticleInsert>;
17+
/** A field.timestamp instant in UTC (`YYYY-MM-DDTHH:MM:SS[.fff]Z`): a value already spelled so is kept
18+
* as sent, an offset is applied, a zoneless value is UTC and a date alone is midnight UTC.
19+
* SQLite/D1 compare timestamps as text, so instants are stored in UTC. Generated. */
20+
function utcIsoTimestamp(v: string): string {
21+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(v)) {
22+
return v;
23+
}
24+
const m =
25+
/^(\d{4}-\d{2}-\d{2})(?:[Tt ](\d{2}:\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:([Zz])|([+-]\d{2}):?(\d{2})?)?)?$/.exec(
26+
v,
27+
);
28+
if (m === null) {
29+
return v;
30+
}
31+
const t = Date.parse(
32+
`${m[1]}T${m[2] ?? "00:00"}:${m[3] ?? "00"}.${(m[4] ?? "").slice(0, 3).padEnd(3, "0")}${
33+
m[6] === undefined ? "Z" : `${m[6]}:${m[7] ?? "00"}`
34+
}`,
35+
);
36+
return Number.isNaN(t)
37+
? v
38+
: new Date(t).toISOString().replace(/\.?0+Z$/, "Z");
39+
}
40+
41+
export const ArticleInsertSchema = z.object({
42+
title: z.string().min(1).max(200),
43+
createdAt: z
44+
.string()
45+
.optional()
46+
.transform(() => new Date().toISOString()),
47+
updatedAt: z
48+
.string()
49+
.optional()
50+
.transform(() => new Date().toISOString()),
51+
});
52+
53+
export const ArticleUpdateSchema = z.object({
54+
title: z.string().min(1).max(200).optional(),
55+
updatedAt: z
56+
.string()
57+
.optional()
58+
.transform(() => new Date().toISOString()),
59+
});
60+
61+
/** Typed create shape for Article: the insert schema's INPUT (pre-transform) type. A
62+
* renamed/dropped/misspelt field is a compile error at every `createArticle` call site;
63+
* the schema still validates at runtime. */
64+
export type ArticleCreate = z.input<typeof ArticleInsertSchema>;
65+
66+
/** Typed patch shape for Article: every settable field, optional (FR-035 PATCH). A
67+
* renamed/dropped field is a compile error at every `updateArticle` call site. */
68+
export type ArticlePatch = z.input<typeof ArticleUpdateSchema>;
69+
70+
/** Insert-shape for import / restore / replication of Article: identical to
71+
* ArticleInsertSchema, but the @autoSet timestamp columns are written VERBATIM
72+
* (no create-time now() stamp) so the caller's original values are preserved. */
73+
export const ArticleInsertPreservingSchema = z.object({
74+
title: z.string().min(1).max(200),
75+
createdAt: z
76+
.string()
77+
.regex(
78+
/^(?:\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|02-(?:0[1-9]|1\d|2[0-8]))|(?:\d{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29)(?:[Tt ](?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:[Zz]|[+-](?:[01]\d|2[0-3])(?::?[0-5]\d)?)?)?$/,
79+
"must be an ISO 8601 timestamp (YYYY-MM-DD[THH:MM[:SS[.fff]]][Z|±HH:MM])",
80+
)
81+
.transform(utcIsoTimestamp)
82+
.optional(),
83+
updatedAt: z
84+
.string()
85+
.regex(
86+
/^(?:\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|02-(?:0[1-9]|1\d|2[0-8]))|(?:\d{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29)(?:[Tt ](?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:[Zz]|[+-](?:[01]\d|2[0-3])(?::?[0-5]\d)?)?)?$/,
87+
"must be an ISO 8601 timestamp (YYYY-MM-DD[THH:MM[:SS[.fff]]][Z|±HH:MM])",
88+
)
89+
.transform(utcIsoTimestamp)
90+
.optional(),
91+
});
92+
93+
/** Typed input of `insertPreservingArticle` — ArticleInsertPreservingSchema's pre-transform shape. */
94+
export type ArticleCreatePreserving = z.input<
95+
typeof ArticleInsertPreservingSchema
96+
>;
97+
/**
98+
* Metadata constants for Article.
99+
*
100+
* Use these instead of magic strings so TS catches typos and refactors stay
101+
* coherent. Each non-dollar-prefixed key is a per-field object carrying
102+
* name, label, view, an htmlType where the field's view maps to one, and the
103+
* RHF-shaped validation rules derived from the field's validator children.
104+
*
105+
* placeholder and helpText are NOT emitted — no provider registers an attribute
106+
* for them, so nothing here could derive one. (htmlType is emitted, but derived
107+
* from the view subtype; the @htmlType override was removed for the same reason.)
108+
* useEntityForm still honours placeholder if you add it: this file is generated,
109+
* hand edits inside it survive regeneration through the three-way merge, and the
110+
* consumers read this const rather than the metadata. That is the intended way
111+
* to set it.
112+
*
113+
* Typical usage with the metaobjects React form helper:
114+
*
115+
* import { useEntityForm } from '@metaobjectsdev/react';
116+
* const form = useEntityForm(Article, ArticleInsertSchema);
117+
* <input {...form.input.id} />
118+
*/
119+
export const Article = {
120+
$entity: "Article",
121+
$table: "articles",
122+
$path: "/articles",
123+
id: { name: "id", label: "Id", view: "number", htmlType: "number" },
124+
title: {
125+
name: "title",
126+
label: "Title",
127+
view: "text",
128+
htmlType: "text",
129+
rules: {
130+
required: "Title is required",
131+
maxLength: { value: 200, message: "Must be 200 characters or fewer" },
132+
},
133+
},
134+
createdAt: {
135+
name: "createdAt",
136+
label: "Created At",
137+
view: "date",
138+
htmlType: "datetime-local",
139+
},
140+
updatedAt: {
141+
name: "updatedAt",
142+
label: "Updated At",
143+
view: "date",
144+
htmlType: "datetime-local",
145+
},
146+
} as const;
147+
import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
148+
149+
export const ArticleFilterAllowlist = {} as const satisfies FilterAllowlist;
150+
import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
151+
152+
export const ArticleSortAllowlist = {} as const satisfies SortAllowlist;
153+
export type ArticleFilter = {
154+
limit?: number;
155+
offset?: number;
156+
sort?: string;
157+
or?: ArticleFilter[];
158+
and?: ArticleFilter[];
159+
};

0 commit comments

Comments
 (0)