hekireki 0.2.5 → 0.2.7

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 CHANGED
@@ -191,9 +191,15 @@ erDiagram
191
191
  ```elixir
192
192
  defmodule DBSchema.User do
193
193
  use Ecto.Schema
194
- @primary_key false
194
+
195
+ @primary_key {:id, :binary_id, autogenerate: true}
196
+
197
+ @type t :: %__MODULE__{
198
+ id: Ecto.UUID.t(),
199
+ name: String.t()
200
+ }
201
+
195
202
  schema "user" do
196
- field(:id, :binary_id, primary_key: true)
197
203
  field(:name, :string)
198
204
  end
199
205
  end
@@ -202,9 +208,17 @@ end
202
208
  ```elixir
203
209
  defmodule DBSchema.Post do
204
210
  use Ecto.Schema
205
- @primary_key false
211
+
212
+ @primary_key {:id, :binary_id, autogenerate: true}
213
+
214
+ @type t :: %__MODULE__{
215
+ id: Ecto.UUID.t(),
216
+ title: String.t(),
217
+ content: String.t(),
218
+ userId: String.t()
219
+ }
220
+
206
221
  schema "post" do
207
- field(:id, :binary_id, primary_key: true)
208
222
  field(:title, :string)
209
223
  field(:content, :string)
210
224
  field(:userId, :string)
@@ -2,70 +2,107 @@ import fsp from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { snakeCase } from '../../../shared/utils/index.js';
4
4
  import { prismaTypeToEctoType } from '../utils/prisma-type-to-ecto-type.js';
5
- /* ───────── Utilities ────────────────────────── */
6
- /** UUID PK :binary_id / otherwise :string */
7
- function getPrimaryKeyType(field) {
5
+ function getPrimaryKeyConfig(field) {
6
+ if (field.type === 'String' &&
7
+ field.default &&
8
+ typeof field.default === 'object' &&
9
+ 'name' in field.default &&
10
+ field.default.name === 'uuid') {
11
+ return {
12
+ line: '@primary_key {:id, :binary_id, autogenerate: true}',
13
+ typeSpec: 'Ecto.UUID.t()',
14
+ omitIdFieldInSchema: true,
15
+ };
16
+ }
17
+ return {
18
+ line: '@primary_key false',
19
+ typeSpec: 'String.t()',
20
+ omitIdFieldInSchema: false,
21
+ };
22
+ }
23
+ function getFieldDefaultOption(field) {
8
24
  const def = field.default;
9
- return def && typeof def === 'object' && 'name' in def && def.name === 'uuid'
10
- ? 'binary_id'
11
- : 'string';
25
+ if (def === undefined || def === null)
26
+ return null;
27
+ if (typeof def === 'string')
28
+ return `default: "${def}"`;
29
+ if (typeof def === 'number' || typeof def === 'boolean')
30
+ return `default: ${def}`;
31
+ return null;
12
32
  }
13
- /** Convert readonly models to mutable copies */
14
- function makeMutable(models) {
15
- return models.map((m) => ({
16
- ...m,
17
- fields: m.fields?.map((f) => ({ ...f })) ?? [],
18
- }));
33
+ function ectoTypeToTypespec(type) {
34
+ switch (type) {
35
+ case 'string': return 'String.t()';
36
+ case 'integer': return 'integer()';
37
+ case 'float': return 'float()';
38
+ case 'boolean': return 'boolean()';
39
+ case 'binary_id': return 'Ecto.UUID.t()';
40
+ case 'naive_datetime': return 'NaiveDateTime.t()';
41
+ case 'utc_datetime': return 'DateTime.t()';
42
+ default: return 'term()';
43
+ }
19
44
  }
20
- /* ───────── Main generator ───────────────────── */
21
- export function ectoSchemas(models, app) {
22
- const mutableModels = makeMutable(models);
23
- /** Timestamp column aliases (snake_case & camelCase) */
45
+ function buildTimestampsLine(fields) {
24
46
  const insertedAliases = ['inserted_at', 'created_at', 'createdAt'];
25
47
  const updatedAliases = ['updated_at', 'modified_at', 'updatedAt', 'modifiedAt'];
26
- return mutableModels
48
+ const inserted = fields.find((f) => insertedAliases.includes(f.name));
49
+ const updated = fields.find((f) => updatedAliases.includes(f.name));
50
+ const exclude = new Set();
51
+ if (inserted)
52
+ exclude.add(inserted.name);
53
+ if (updated)
54
+ exclude.add(updated.name);
55
+ if (!(inserted || updated))
56
+ return { line: null, exclude };
57
+ if (inserted?.name === 'inserted_at' && updated?.name === 'updated_at') {
58
+ return { line: ' timestamps()', exclude };
59
+ }
60
+ return {
61
+ line: ` timestamps(inserted_at: :${inserted?.name ?? 'inserted_at'}, updated_at: :${updated?.name ?? 'updated_at'})`,
62
+ exclude,
63
+ };
64
+ }
65
+ export function ectoSchemas(models, app) {
66
+ return models
27
67
  .map((model) => {
28
- /* ── Primary-key handling ─────────────────── */
29
- const idFields = model.fields.filter((f) => f.isId);
30
- const isCompositePK = model.primaryKey && model.primaryKey.fields.length > 1;
31
- if (!(idFields.length || isCompositePK))
68
+ const idField = model.fields.find((f) => f.isId);
69
+ if (!idField)
32
70
  return '';
33
- const pkField = idFields[0];
34
- const pkType = pkField ? getPrimaryKeyType(pkField) : 'id';
35
- /* ── Timestamp field detection ────────────── */
36
- const insertedField = model.fields.find((f) => insertedAliases.includes(f.name));
37
- const updatedField = model.fields.find((f) => updatedAliases.includes(f.name));
38
- /** Columns removed from explicit `field/3` declarations */
39
- const excludedNames = [
40
- ...(insertedField ? [insertedField.name] : []),
41
- ...(updatedField ? [updatedField.name] : []),
71
+ const pk = getPrimaryKeyConfig(idField);
72
+ const fields = model.fields.map((f) => ({ ...f }));
73
+ const { line: timestampsLine, exclude: timestampsExclude } = buildTimestampsLine(fields);
74
+ const schemaFieldsRaw = fields.filter((f) => !(f.relationName ||
75
+ (f.isId && pk.omitIdFieldInSchema) ||
76
+ timestampsExclude.has(f.name)));
77
+ const typeSpecFields = [
78
+ `id: ${pk.typeSpec}`,
79
+ ...schemaFieldsRaw.map((f) => `${f.name}: ${ectoTypeToTypespec(prismaTypeToEctoType(f.type))}`),
42
80
  ];
43
- /* ── Plain fields (no relations / no timestamps) ─ */
44
- const fields = model.fields.filter((f) => !(f.relationName || excludedNames.includes(f.name)));
45
- /* ── Build timestamps() line (const-only) ─────── */
46
- const timestampsLine = (() => {
47
- if (!(insertedField || updatedField))
48
- return '';
49
- const hasCustom = (insertedField && insertedField.name !== 'inserted_at') ||
50
- (updatedField && updatedField.name !== 'updated_at');
51
- if (!hasCustom)
52
- return ' timestamps()'; // both defaults short form
53
- // Always include both keys when custom names are involved
54
- const insertedName = insertedField ? insertedField.name : 'inserted_at';
55
- const updatedName = updatedField ? updatedField.name : 'updated_at';
56
- return ` timestamps(inserted_at: :${insertedName}, updated_at: :${updatedName})`;
57
- })();
58
- /* ── Assemble final module code ───────────── */
81
+ const typeSpecLines = [
82
+ ' @type t :: %__MODULE__{',
83
+ ...typeSpecFields.map((line, i) => {
84
+ const isLast = i === typeSpecFields.length - 1;
85
+ return ` ${line}${isLast ? '' : ','}`;
86
+ }),
87
+ ' }',
88
+ ];
89
+ const schemaFields = schemaFieldsRaw.map((f) => {
90
+ const type = f.isId ? 'binary_id' : prismaTypeToEctoType(f.type);
91
+ const primary = f.isId && !pk.omitIdFieldInSchema ? ', primary_key: true' : '';
92
+ const defaultOpt = getFieldDefaultOption(f);
93
+ const defaultClause = defaultOpt ? `, ${defaultOpt}` : '';
94
+ return ` field(:${f.name}, :${type}${primary}${defaultClause})`;
95
+ });
59
96
  const lines = [
60
97
  `defmodule ${app}.${model.name} do`,
61
98
  ' use Ecto.Schema',
62
- ' @primary_key false',
99
+ '',
100
+ ` ${pk.line}`,
101
+ '',
102
+ ...typeSpecLines,
103
+ '',
63
104
  ` schema "${snakeCase(model.name)}" do`,
64
- ...fields.map((f) => {
65
- const type = f.isId ? pkType : prismaTypeToEctoType(f.type);
66
- const primary = f.isId && !isCompositePK ? ', primary_key: true' : '';
67
- return ` field(:${f.name}, :${type}${primary})`;
68
- }),
105
+ ...schemaFields,
69
106
  ...(timestampsLine ? [timestampsLine] : []),
70
107
  ' end',
71
108
  'end',
@@ -75,11 +112,9 @@ export function ectoSchemas(models, app) {
75
112
  .filter(Boolean)
76
113
  .join('\n\n');
77
114
  }
78
- /* ───────── File writer ───────────────────────── */
79
115
  export async function writeEctoSchemasToFiles(models, app, outDir) {
80
- const mutableModels = makeMutable(models);
81
116
  await fsp.mkdir(outDir, { recursive: true });
82
- for (const model of mutableModels) {
117
+ for (const model of models) {
83
118
  const code = ectoSchemas([model], app);
84
119
  if (!code.trim())
85
120
  continue;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hekireki",
3
3
  "type": "module",
4
- "version": "0.2.5",
4
+ "version": "0.2.7",
5
5
  "license": "MIT",
6
6
  "description": "Hekireki is a tool that generates validation schemas for Zod and Valibot, as well as ER diagrams, from Prisma schemas annotated with comments.",
7
7
  "keywords": [