runeforge 0.0.7 → 0.0.8

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
@@ -284,6 +284,14 @@ The `load` function returns a single record when `?id=` is present (used by the
284
284
 
285
285
  `dataKey` must match the key returned by the load function for the list. Each `endpoint` maps to a SvelteKit form action on the same page.
286
286
 
287
+ If your records use a different identifier field than `_id` (e.g. a plain `id`), pass the `idKey` prop:
288
+
289
+ ```svelte
290
+ <GenericCRUD idKey="id" ... />
291
+ ```
292
+
293
+ This propagates to navigation URLs, form submissions, deletion calls, and the auto-excluded column list, so no other changes are needed on your end.
294
+
287
295
  ---
288
296
 
289
297
  ## Components
@@ -18,6 +18,7 @@
18
18
  let {
19
19
  data = undefined as Record<string, unknown> | undefined,
20
20
  dataKey = undefined as string | undefined,
21
+ idKey = '_id',
21
22
  labelOne = '',
22
23
  labelMany = '',
23
24
  icon,
@@ -34,6 +35,7 @@
34
35
  }: {
35
36
  data?: Record<string, unknown>;
36
37
  dataKey?: string;
38
+ idKey?: string;
37
39
  labelOne?: string;
38
40
  labelMany?: string;
39
41
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -61,9 +63,11 @@
61
63
  const reading = $derived(idParam !== null && viewParam === null);
62
64
  const editing = $derived(idParam !== null && viewParam === 'edit');
63
65
 
66
+ const excluded = $derived(new Set([...AUTO_EXCLUDED, idKey]));
67
+
64
68
  const singleInstance = $derived<T | undefined>(
65
69
  (Object.values(page.data as Record<string, unknown>).find(
66
- (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && '_id' in (v as object)
70
+ (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && idKey in (v as object)
67
71
  ) as T | undefined)
68
72
  );
69
73
 
@@ -72,10 +76,10 @@
72
76
  async function navList() { await goto('?'); }
73
77
  async function navCreate() { await goto('?view=create'); }
74
78
  async function navRead(item: T) {
75
- await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)._id ?? ''))}`);
79
+ await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)[idKey] ?? ''))}`);
76
80
  }
77
81
  async function navEdit(item: T) {
78
- await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)._id ?? ''))}&view=edit`);
82
+ await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)[idKey] ?? ''))}&view=edit`);
79
83
  }
80
84
 
81
85
  const resolvedColumns: ColumnDefinition<T>[] = $derived(
@@ -93,7 +97,7 @@
93
97
  }))
94
98
  : entityData.length > 0
95
99
  ? (Object.keys(entityData[0]) as (keyof T & string)[])
96
- .filter((k) => k !== '_id')
100
+ .filter((k) => !excluded.has(k))
97
101
  .map((k) => ({ attribute: k, title: k }))
98
102
  : [])
99
103
  );
@@ -101,7 +105,7 @@
101
105
  const resolvedFields: FieldDefinition<T>[] = $derived(
102
106
  fields ?? (meta
103
107
  ? (Object.entries(meta) as [string, AttributeMetadata][])
104
- .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromCreate)
108
+ .filter(([k, m]) => !excluded.has(k) && !m.excludedFromCreate)
105
109
  .map(([k, m]) => ({
106
110
  attribute: k as keyof T & string,
107
111
  title: m.label,
@@ -114,7 +118,7 @@
114
118
  }))
115
119
  : entityData.length > 0
116
120
  ? (Object.entries(entityData[0]) as [string, unknown][])
117
- .filter(([k]) => !AUTO_EXCLUDED.has(k))
121
+ .filter(([k]) => !excluded.has(k))
118
122
  .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
119
123
  : [])
120
124
  );
@@ -122,7 +126,7 @@
122
126
  const resolvedReadFields: FieldDefinition<T>[] = $derived(
123
127
  fields ?? (meta
124
128
  ? (Object.entries(meta) as [string, AttributeMetadata][])
125
- .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromRead)
129
+ .filter(([k, m]) => !excluded.has(k) && !m.excludedFromRead)
126
130
  .map(([k, m]) => ({
127
131
  attribute: k as keyof T & string,
128
132
  title: m.label,
@@ -135,7 +139,7 @@
135
139
  }))
136
140
  : entityData.length > 0
137
141
  ? (Object.entries(entityData[0]) as [string, unknown][])
138
- .filter(([k]) => !AUTO_EXCLUDED.has(k))
142
+ .filter(([k]) => !excluded.has(k))
139
143
  .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
140
144
  : [])
141
145
  );
@@ -143,7 +147,7 @@
143
147
  const resolvedUpdateFields: FieldDefinition<T>[] = $derived(
144
148
  fields ?? (meta
145
149
  ? (Object.entries(meta) as [string, AttributeMetadata][])
146
- .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromUpdate)
150
+ .filter(([k, m]) => !excluded.has(k) && !m.excludedFromUpdate)
147
151
  .map(([k, m]) => ({
148
152
  attribute: k as keyof T & string,
149
153
  title: m.label,
@@ -156,7 +160,7 @@
156
160
  }))
157
161
  : entityData.length > 0
158
162
  ? (Object.entries(entityData[0]) as [string, unknown][])
159
- .filter(([k]) => !AUTO_EXCLUDED.has(k))
163
+ .filter(([k]) => !excluded.has(k))
160
164
  .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
161
165
  : [])
162
166
  );
@@ -184,6 +188,7 @@
184
188
  {labelOne}
185
189
  {labelMany}
186
190
  {icon}
191
+ {idKey}
187
192
  fields={resolvedReadFields}
188
193
  instance={singleInstance ?? {} as T}
189
194
  {read}
@@ -194,6 +199,7 @@
194
199
  {labelOne}
195
200
  {labelMany}
196
201
  {icon}
202
+ {idKey}
197
203
  fields={resolvedUpdateFields}
198
204
  instance={singleInstance ?? {} as T}
199
205
  {update}
@@ -208,6 +214,7 @@
208
214
  {labelMany}
209
215
  {icon}
210
216
  {pageSize}
217
+ {idKey}
211
218
  {creation}
212
219
  {update}
213
220
  {read}
@@ -4,6 +4,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
4
4
  props: {
5
5
  data?: Record<string, unknown>;
6
6
  dataKey?: string;
7
+ idKey?: string;
7
8
  labelOne?: string;
8
9
  labelMany?: string;
9
10
  icon?: any;
@@ -1,6 +1,4 @@
1
1
  export declare const formatBoolean: (trueLabel?: string, falseLabel?: string) => () => (value: boolean) => string;
2
2
  export declare const formatDatetime: (format?: string) => () => (value: Date) => string;
3
3
  export declare function formatTruncateTextUpTo(maxLength: number): () => (value: string) => string;
4
- export declare function formatInstance<T extends {
5
- _id: string;
6
- }>(attribute: keyof T & string, instances: T[], urlPath: string): (value: unknown) => string;
4
+ export declare function formatInstance<T extends Record<string, unknown>>(attribute: keyof T & string, instances: T[], urlPath: string, idKey?: keyof T & string): (value: unknown) => string;
@@ -30,8 +30,8 @@ function escapeHtml(str) {
30
30
  .replace(/>/g, '&gt;')
31
31
  .replace(/"/g, '&quot;');
32
32
  }
33
- export function formatInstance(attribute, instances, urlPath) {
34
- const map = new Map(instances.map((i) => [i._id, i]));
33
+ export function formatInstance(attribute, instances, urlPath, idKey = '_id') {
34
+ const map = new Map(instances.map((i) => [String(i[idKey] ?? ''), i]));
35
35
  return (value) => {
36
36
  const id = String(value ?? '');
37
37
  const instance = map.get(id);
@@ -22,6 +22,7 @@
22
22
  labelMany = '',
23
23
  icon,
24
24
  pageSize = 10,
25
+ idKey = '_id',
25
26
  creation = {} as ActionConfiguration<T>,
26
27
  update = {} as ActionConfiguration<T>,
27
28
  read = {} as ActionConfiguration<T>,
@@ -39,6 +40,7 @@
39
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
41
  icon?: any;
41
42
  pageSize?: number;
43
+ idKey?: string;
42
44
  creation?: ActionConfiguration<T>;
43
45
  update?: ActionConfiguration<T>;
44
46
  read?: ActionConfiguration<T>;
@@ -67,7 +69,7 @@
67
69
  async function runEndpointAction(endpoint: string, items: T[]) {
68
70
  await Promise.all(items.map((item) => {
69
71
  const fd = new FormData();
70
- fd.set('id', String((item as Record<string, unknown>)._id ?? ''));
72
+ fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
71
73
  return fetch(endpoint, { method: 'POST', body: fd });
72
74
  }));
73
75
  await invalidateAll();
@@ -6,6 +6,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
6
6
  labelMany?: string;
7
7
  icon?: any;
8
8
  pageSize?: number;
9
+ idKey?: string;
9
10
  creation?: ActionConfiguration<T>;
10
11
  update?: ActionConfiguration<T>;
11
12
  read?: ActionConfiguration<T>;
@@ -15,6 +15,7 @@
15
15
  labelOne = '',
16
16
  labelMany = '',
17
17
  icon,
18
+ idKey = '_id',
18
19
  fields = [] as FieldDefinition<T>[],
19
20
  instance = {} as T,
20
21
  read = {} as ActionConfiguration<T>,
@@ -24,6 +25,7 @@
24
25
  labelMany?: string;
25
26
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
27
  icon?: any;
28
+ idKey?: string;
27
29
  fields?: FieldDefinition<T>[];
28
30
  instance?: T;
29
31
  read?: ActionConfiguration<T>;
@@ -37,7 +39,7 @@
37
39
 
38
40
  $effect(() => {
39
41
  const endpoint = read.endpoint;
40
- const id = (instance as Record<string, unknown>)._id;
42
+ const id = (instance as Record<string, unknown>)[idKey];
41
43
  if (!endpoint || id == null) return;
42
44
 
43
45
  let cancelled = false;
@@ -50,7 +52,7 @@
50
52
  const data = result.data as Record<string, unknown> | undefined;
51
53
  const fetched = data
52
54
  ? Object.values(data).find(
53
- (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && '_id' in (v as object)
55
+ (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && idKey in (v as object)
54
56
  )
55
57
  : undefined;
56
58
  if (fetched && typeof fetched === 'object') record = fetched as Record<string, unknown>;
@@ -4,6 +4,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
4
4
  labelOne?: string;
5
5
  labelMany?: string;
6
6
  icon?: any;
7
+ idKey?: string;
7
8
  fields?: FieldDefinition<T>[];
8
9
  instance?: T;
9
10
  read?: ActionConfiguration<T>;
@@ -16,6 +16,7 @@
16
16
  labelOne = '',
17
17
  labelMany = '',
18
18
  icon,
19
+ idKey = '_id',
19
20
  fields = [] as FieldDefinition<T>[],
20
21
  instance = {} as T,
21
22
  update = {} as ActionConfiguration<T>,
@@ -27,6 +28,7 @@
27
28
  labelMany?: string;
28
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
30
  icon?: any;
31
+ idKey?: string;
30
32
  fields?: FieldDefinition<T>[];
31
33
  instance?: T;
32
34
  update?: ActionConfiguration<T>;
@@ -56,7 +58,7 @@
56
58
  );
57
59
 
58
60
  $effect(() => {
59
- const id = (instance as Record<string, unknown>)._id;
61
+ const id = (instance as Record<string, unknown>)[idKey];
60
62
  if (!id) return;
61
63
  untrack(() => { record = seedFromInstance(instance as Record<string, unknown>); });
62
64
  });
@@ -130,7 +132,7 @@
130
132
  };
131
133
  }}
132
134
  >
133
- <input type="hidden" name="id" value={String(record._id ?? '')} />
135
+ <input type="hidden" name="id" value={String(record[idKey] ?? '')} />
134
136
 
135
137
  {#each fields as field (field.attribute)}
136
138
  <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
@@ -4,6 +4,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
4
4
  labelOne?: string;
5
5
  labelMany?: string;
6
6
  icon?: any;
7
+ idKey?: string;
7
8
  fields?: FieldDefinition<T>[];
8
9
  instance?: T;
9
10
  update?: ActionConfiguration<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runeforge",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "SvelteKit toolkit for building metadata-driven CRUD interfaces with tables, forms, and actions",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",