@devoplus/one-sdk 0.0.0-stage → 0.2.0-next.112

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.
Files changed (139) hide show
  1. package/CHANGELOG.md +268 -0
  2. package/README.md +894 -2
  3. package/bin/accessRequest.d.mts +96 -0
  4. package/bin/accessRequest.mjs +340 -0
  5. package/bin/appOps.d.mts +97 -0
  6. package/bin/appOps.mjs +336 -0
  7. package/bin/dataCommand.mjs +131 -0
  8. package/bin/devConfig.mjs +132 -0
  9. package/bin/one.mjs +341 -0
  10. package/bin/session.mjs +239 -0
  11. package/dist/agentInput.d.ts +9 -0
  12. package/dist/agentInput.d.ts.map +1 -0
  13. package/dist/agentInput.js +102 -0
  14. package/dist/agentInput.js.map +1 -0
  15. package/dist/agentTools.d.ts +83 -0
  16. package/dist/agentTools.d.ts.map +1 -0
  17. package/dist/agentTools.js +164 -0
  18. package/dist/agentTools.js.map +1 -0
  19. package/dist/ai.d.ts +67 -0
  20. package/dist/ai.d.ts.map +1 -0
  21. package/dist/ai.js +129 -0
  22. package/dist/ai.js.map +1 -0
  23. package/dist/audit.d.ts +21 -0
  24. package/dist/audit.d.ts.map +1 -0
  25. package/dist/audit.js +58 -0
  26. package/dist/audit.js.map +1 -0
  27. package/dist/backend.d.ts +27 -0
  28. package/dist/backend.d.ts.map +1 -0
  29. package/dist/backend.js +63 -0
  30. package/dist/backend.js.map +1 -0
  31. package/dist/cache.d.ts +20 -0
  32. package/dist/cache.d.ts.map +1 -0
  33. package/dist/cache.js +84 -0
  34. package/dist/cache.js.map +1 -0
  35. package/dist/client.d.ts +147 -0
  36. package/dist/client.d.ts.map +1 -0
  37. package/dist/client.js +469 -0
  38. package/dist/client.js.map +1 -0
  39. package/dist/components.d.ts +235 -0
  40. package/dist/components.d.ts.map +1 -0
  41. package/dist/components.js +1491 -0
  42. package/dist/components.js.map +1 -0
  43. package/dist/context.d.ts +110 -0
  44. package/dist/context.d.ts.map +1 -0
  45. package/dist/context.js +126 -0
  46. package/dist/context.js.map +1 -0
  47. package/dist/data/crud.d.ts +74 -0
  48. package/dist/data/crud.d.ts.map +1 -0
  49. package/dist/data/crud.js +614 -0
  50. package/dist/data/crud.js.map +1 -0
  51. package/dist/data/ddl.d.ts +76 -0
  52. package/dist/data/ddl.d.ts.map +1 -0
  53. package/dist/data/ddl.js +362 -0
  54. package/dist/data/ddl.js.map +1 -0
  55. package/dist/data/fold.d.ts +14 -0
  56. package/dist/data/fold.d.ts.map +1 -0
  57. package/dist/data/fold.js +76 -0
  58. package/dist/data/fold.js.map +1 -0
  59. package/dist/data/model.d.ts +113 -0
  60. package/dist/data/model.d.ts.map +1 -0
  61. package/dist/data/model.js +408 -0
  62. package/dist/data/model.js.map +1 -0
  63. package/dist/data/values.d.ts +29 -0
  64. package/dist/data/values.d.ts.map +1 -0
  65. package/dist/data/values.js +152 -0
  66. package/dist/data/values.js.map +1 -0
  67. package/dist/dexa.d.ts +44 -0
  68. package/dist/dexa.d.ts.map +1 -0
  69. package/dist/dexa.js +74 -0
  70. package/dist/dexa.js.map +1 -0
  71. package/dist/dexaCommand.d.ts +116 -0
  72. package/dist/dexaCommand.d.ts.map +1 -0
  73. package/dist/dexaCommand.js +226 -0
  74. package/dist/dexaCommand.js.map +1 -0
  75. package/dist/export/columns.d.ts +62 -0
  76. package/dist/export/columns.d.ts.map +1 -0
  77. package/dist/export/columns.js +127 -0
  78. package/dist/export/columns.js.map +1 -0
  79. package/dist/export/csv.d.ts +10 -0
  80. package/dist/export/csv.d.ts.map +1 -0
  81. package/dist/export/csv.js +30 -0
  82. package/dist/export/csv.js.map +1 -0
  83. package/dist/export/report.d.ts +19 -0
  84. package/dist/export/report.d.ts.map +1 -0
  85. package/dist/export/report.js +91 -0
  86. package/dist/export/report.js.map +1 -0
  87. package/dist/export/response.d.ts +14 -0
  88. package/dist/export/response.d.ts.map +1 -0
  89. package/dist/export/response.js +70 -0
  90. package/dist/export/response.js.map +1 -0
  91. package/dist/export/xlsx.d.ts +15 -0
  92. package/dist/export/xlsx.d.ts.map +1 -0
  93. package/dist/export/xlsx.js +167 -0
  94. package/dist/export/xlsx.js.map +1 -0
  95. package/dist/export/zip.d.ts +7 -0
  96. package/dist/export/zip.d.ts.map +1 -0
  97. package/dist/export/zip.js +83 -0
  98. package/dist/export/zip.js.map +1 -0
  99. package/dist/files.d.ts +52 -0
  100. package/dist/files.d.ts.map +1 -0
  101. package/dist/files.js +137 -0
  102. package/dist/files.js.map +1 -0
  103. package/dist/index.d.ts +31 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +35 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/jobs.d.ts +57 -0
  108. package/dist/jobs.d.ts.map +1 -0
  109. package/dist/jobs.js +157 -0
  110. package/dist/jobs.js.map +1 -0
  111. package/dist/log.d.ts +22 -0
  112. package/dist/log.d.ts.map +1 -0
  113. package/dist/log.js +107 -0
  114. package/dist/log.js.map +1 -0
  115. package/dist/notify.d.ts +41 -0
  116. package/dist/notify.d.ts.map +1 -0
  117. package/dist/notify.js +11 -0
  118. package/dist/notify.js.map +1 -0
  119. package/dist/org.d.ts +184 -0
  120. package/dist/org.d.ts.map +1 -0
  121. package/dist/org.js +114 -0
  122. package/dist/org.js.map +1 -0
  123. package/dist/people.d.ts +39 -0
  124. package/dist/people.d.ts.map +1 -0
  125. package/dist/people.js +88 -0
  126. package/dist/people.js.map +1 -0
  127. package/dist/problem.d.ts +12 -0
  128. package/dist/problem.d.ts.map +1 -0
  129. package/dist/problem.js +33 -0
  130. package/dist/problem.js.map +1 -0
  131. package/dist/runtime.d.ts +92 -0
  132. package/dist/runtime.d.ts.map +1 -0
  133. package/dist/runtime.js +8 -0
  134. package/dist/runtime.js.map +1 -0
  135. package/dist/scenarios.d.ts +125 -0
  136. package/dist/scenarios.d.ts.map +1 -0
  137. package/dist/scenarios.js +251 -0
  138. package/dist/scenarios.js.map +1 -0
  139. package/package.json +55 -3
@@ -0,0 +1,614 @@
1
+ // Kayıt işlemleri (D-84): `one.data.json`'daki bir varlık için hazır liste/okuma/oluşturma/güncelleme/silme uçları +
2
+ // dışa aktarma (D-85). Uygulama yazmaz, yapılandırır:
3
+ //
4
+ // const requests = createCrudHandler({ model: dataModel, entity: 'requests', records: 'shared', canWrite: 'One.Talep.Write' })
5
+ // const res = await requests.handle(request, env.DB); if (res) return res
6
+ //
7
+ // Uçlar (`basePath` varsayılan `/api`):
8
+ // GET /api/<varlık> sayfalı liste — ?_page=1&_size=50&_sort=-created_at · <alan>=<değer> (eşitlik) ·
9
+ // <alan>.contains=<metin> (metin içerir; Türkçe katlanmış — büyük/küçük harf ve
10
+ // ç/ğ/ı/ö/ş/ü duyarsız, S15) · <alan>.from / <alan>.to (aralık, dahil)
11
+ // POST /api/<varlık> kayıt oluşturur (201)
12
+ // GET /api/<varlık>/<id> tek kayıt
13
+ // PATCH /api/<varlık>/<id> gönderilen alanları günceller
14
+ // DELETE /api/<varlık>/<id> siler (varsayılan yumuşak silme: `deleted_at` dolar) — 204
15
+ // GET /api/<varlık>/export.csv|export.xlsx listeyle aynı süzgeçlerle dışa aktarma (?_lang=tr|en)
16
+ // GET /api/<varlık>/report.html yazdırmaya uygun rapor sayfası (aynı süzgeçler)
17
+ //
18
+ // ⛔ Güvenlik:
19
+ // • SQL'e yalnız MODELDEKİ adlar (tırnaklı) girer; her değer bağlı parametredir (`?`). Sıralama alanı da modelden seçilir.
20
+ // • `created_by` / `updated_by` oturumdan yazılır; gövdedeki sistem alanı (id, created_by …) REDDEDİLİR.
21
+ // • Okuma ve yazma oturum ister (401). `canRead` / `canWrite` uygulama izni (`hasPermission`) ya da işlev alır (403).
22
+ // • Vekalet / Yerine Görüntüle oturumunda yazma varsayılan KAPALIDIR (`actingSessions: 'write'` ile açılır; manifest'te de
23
+ // `actingSessions: "write"` beyan edilmelidir — platform kapısı onsuz isteği zaten durdurur, bu ikinci savunmadır).
24
+ // • `records: 'own'` her sorguya `created_by = <oturumdaki kişi>` koşulunu ekler: kişi yalnız kendi kayıtlarını görür.
25
+ import { OneError, hasPermission, isActingSession, readContext, requireUser } from '../context.js';
26
+ import { logEvent } from '../log.js';
27
+ import { problemResponse } from '../problem.js';
28
+ import { DEFAULT_TIME_ZONE, formatDate } from '../export/columns.js';
29
+ import { toCsv } from '../export/csv.js';
30
+ import { renderPrintableReport, reportResponse } from '../export/report.js';
31
+ import { exportResponse } from '../export/response.js';
32
+ import { toXlsx } from '../export/xlsx.js';
33
+ import { DataModelError, SYSTEM_COLUMNS, parseDataModel } from './model.js';
34
+ import { foldSearchText, foldSqlExpression } from './fold.js';
35
+ import { coerceFilter, coerceInput, defaultDbValue, outputValue } from './values.js';
36
+ export const CRUD_MAX_PAGE_SIZE = 200;
37
+ export const CRUD_MAX_FILTERS = 20;
38
+ export const CRUD_MAX_BODY_BYTES = 1024 * 1024;
39
+ export const CRUD_REPORT_MAX_ROWS = 2_000;
40
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
41
+ /** Alan hatalarını taşıyan hata — yanıtta RFC 9457 gövdesine `errors: [{ field, message }]` olarak eklenir. */
42
+ export class DataValidationError extends OneError {
43
+ errors;
44
+ constructor(status, code, detail, errors) {
45
+ super(status, code, detail);
46
+ this.errors = errors;
47
+ }
48
+ }
49
+ const parsedModels = new WeakMap();
50
+ /** Modeli doğrulamadan (ham `one.data.json` biçiminde) bir varlıkta verilen adda alan var mı? */
51
+ function fieldNameIn(model, entity, field) {
52
+ const fields = model.entities?.[entity]?.fields;
53
+ return fields !== undefined && Object.prototype.hasOwnProperty.call(fields, field);
54
+ }
55
+ function loadModel(model) {
56
+ const cached = parsedModels.get(model);
57
+ if (cached)
58
+ return cached;
59
+ try {
60
+ const parsed = parseDataModel(model);
61
+ parsedModels.set(model, parsed);
62
+ return parsed;
63
+ }
64
+ catch (e) {
65
+ if (e instanceof DataModelError)
66
+ throw new OneError(500, 'ONE-SDK-E-100', `Veri modeli geçersiz: ${e.message}`);
67
+ throw e;
68
+ }
69
+ }
70
+ const q = (name) => `"${name}"`;
71
+ const SYSTEM_SET = new Set(SYSTEM_COLUMNS);
72
+ function allowed(ctx, rule) {
73
+ if (rule === undefined)
74
+ return true;
75
+ return typeof rule === 'string' ? hasPermission(ctx, rule) : rule(ctx) === true;
76
+ }
77
+ function problem(e) {
78
+ const body = { type: 'about:blank', title: e.code, status: e.status, detail: e.message, code: e.code };
79
+ if (e instanceof DataValidationError)
80
+ body.errors = e.errors;
81
+ const headers = new Headers({ 'content-type': 'application/problem+json; charset=utf-8' });
82
+ if (e.status === 405)
83
+ headers.set('allow', e.message.match(/\(([^)]+)\)/)?.[1] ?? '');
84
+ return new Response(JSON.stringify(body), { status: e.status, headers });
85
+ }
86
+ const json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', ...headers } });
87
+ async function readJsonBody(request) {
88
+ const type = (request.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();
89
+ if (type !== 'application/json')
90
+ throw new OneError(415, 'ONE-SDK-E-109', 'Gövde application/json türünde olmalı.');
91
+ const declared = request.headers.get('content-length');
92
+ if (declared !== null && Number(declared) > CRUD_MAX_BODY_BYTES)
93
+ throw new OneError(413, 'ONE-SDK-E-108', 'Gövde 1 MB sınırını aşıyor.');
94
+ const chunks = [];
95
+ let total = 0;
96
+ if (request.body) {
97
+ const reader = request.body.getReader();
98
+ for (;;) {
99
+ const { done, value } = await reader.read();
100
+ if (done)
101
+ break;
102
+ total += value.byteLength;
103
+ if (total > CRUD_MAX_BODY_BYTES) {
104
+ await reader.cancel().catch(() => undefined);
105
+ throw new OneError(413, 'ONE-SDK-E-108', 'Gövde 1 MB sınırını aşıyor.');
106
+ }
107
+ chunks.push(value);
108
+ }
109
+ }
110
+ const bytes = new Uint8Array(total);
111
+ let offset = 0;
112
+ for (const c of chunks) {
113
+ bytes.set(c, offset);
114
+ offset += c.byteLength;
115
+ }
116
+ let parsed;
117
+ try {
118
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
119
+ }
120
+ catch {
121
+ throw new OneError(400, 'ONE-SDK-E-101', 'Gövde geçerli JSON değil.');
122
+ }
123
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
124
+ throw new OneError(400, 'ONE-SDK-E-101', 'Gövde bir JSON nesnesi olmalı.');
125
+ return parsed;
126
+ }
127
+ const RANGE_TYPES = new Set(['number', 'money', 'date', 'datetime']);
128
+ const CONTAINS_TYPES = new Set(['text', 'longText']);
129
+ const SYSTEM_DATETIME = ['created_at', 'updated_at'].map((name) => ({
130
+ name,
131
+ column: name,
132
+ type: 'datetime',
133
+ title: { tr: name === 'created_at' ? 'Oluşturulma' : 'Güncellenme', en: name === 'created_at' ? 'Created' : 'Updated' },
134
+ required: true,
135
+ unique: false,
136
+ index: false,
137
+ default: undefined,
138
+ choices: [],
139
+ maxLength: undefined,
140
+ foldColumn: undefined,
141
+ min: undefined,
142
+ max: undefined,
143
+ }));
144
+ const CREATED_BY_FIELD = { ...SYSTEM_DATETIME[0], name: 'created_by', column: 'created_by', type: 'person', title: { tr: 'Oluşturan', en: 'Created by' } };
145
+ /**
146
+ * `içerir` koşulu (S15): alanın Türkçe katlanmış gölgesinde arar; gölge boşsa (eski ya da elle yazılmış kayıt) aynı
147
+ * katlama veritabanında anında uygulanır. `instr` desen dili kullanmaz: % ve _ joker değildir, kaçış gerekmez.
148
+ * Aranan ifade de AYNI kuralla katlanır (`foldSearchText`) — çağıran onu bağlı parametre olarak ekler.
149
+ */
150
+ function containsClause(f) {
151
+ const fold = f.foldColumn ?? f.column;
152
+ return `instr(coalesce(${q(fold)}, ${foldSqlExpression(f.column)}), ?) > 0`;
153
+ }
154
+ /**
155
+ * Hazır liste bileşeninin (`<one-table src>`, D-83) sorgu lehçesi → bu uçların lehçesi. Bileşen `limit` · `page` ·
156
+ * `cursor` · `sort` + `order` · `q` · `<alan>From` / `<alan>To` · `lang` gönderir; uçlar `_size` · `_page` · `_sort` ·
157
+ * `<alan>.contains` · `<alan>.from` / `<alan>.to` · `_lang` bekler. İki lehçe aynı uçta çalışır: bileşen `src` olarak
158
+ * doğrudan `/api/<varlık>` alabilir (D-82 şablonları bunu kullanır; önce 400 "Bilinmeyen sorgu parametresi" dönüyordu).
159
+ *
160
+ * ⚠️ Lehçe adı modeldeki bir ALAN adıysa (ör. alanın adı `page`) o parametre alan süzgecidir — model önceliklidir
161
+ * (geriye uyum). `q` metin alanlarının (text · longText) HERHANGİ birinde içerir araması yapar (Türkçe katlanmış,
162
+ * `.contains` ile aynı kural). `cursor` bu uçlarda sayfa numarasıdır (yanıttaki `nextCursor`).
163
+ */
164
+ export function translateTableDialect(entity, params) {
165
+ const fieldNames = new Set(entity.fields.map((f) => f.name));
166
+ const out = new URLSearchParams();
167
+ const issues = [];
168
+ let search = null;
169
+ let sort = null;
170
+ let order = null;
171
+ const alias = (from, to, value) => {
172
+ if (params.has(to))
173
+ issues.push({ field: from, message: `"${from}" ile "${to}" birlikte verilemez.` });
174
+ else
175
+ out.append(to, value);
176
+ };
177
+ for (const [key, value] of params) {
178
+ if (fieldNames.has(key)) {
179
+ out.append(key, value);
180
+ continue;
181
+ }
182
+ if (key === 'limit')
183
+ alias(key, '_size', value);
184
+ else if (key === 'page')
185
+ alias(key, '_page', value);
186
+ else if (key === 'cursor') {
187
+ if (!/^\d{1,6}$/.test(value))
188
+ issues.push({ field: key, message: '"cursor" geçersiz (bu uçta sayfa numarasıdır).' });
189
+ else if (params.has('page') || params.has('_page'))
190
+ issues.push({ field: key, message: '"cursor" sayfa parametresiyle birlikte verilemez.' });
191
+ else
192
+ out.append('_page', value);
193
+ }
194
+ else if (key === 'sort' || key === 'order') {
195
+ if (params.getAll(key).length > 1)
196
+ issues.push({ field: key, message: `"${key}" sorgu parametresi bir kez verilebilir.` });
197
+ else if (key === 'sort')
198
+ sort = value;
199
+ else
200
+ order = value;
201
+ }
202
+ else if (key === 'q') {
203
+ if (params.getAll('q').length > 1)
204
+ issues.push({ field: key, message: '"q" sorgu parametresi bir kez verilebilir.' });
205
+ else
206
+ search = value;
207
+ }
208
+ else if (key === 'lang')
209
+ alias(key, '_lang', value);
210
+ else {
211
+ const m = /^([a-z][a-z0-9_]*)(From|To)$/.exec(key);
212
+ if (m && fieldNames.has(m[1]))
213
+ out.append(`${m[1]}.${m[2] === 'From' ? 'from' : 'to'}`, value);
214
+ else
215
+ out.append(key, value);
216
+ }
217
+ }
218
+ if (order !== null && order !== 'asc' && order !== 'desc')
219
+ issues.push({ field: 'order', message: '"order" asc ya da desc olmalı.' });
220
+ else if (sort !== null)
221
+ alias('sort', '_sort', `${order === 'desc' ? '-' : ''}${sort}`);
222
+ else if (order !== null)
223
+ issues.push({ field: 'order', message: '"order" yalnız "sort" ile verilir.' });
224
+ if (search !== null) {
225
+ const trimmed = search.trim();
226
+ if (trimmed === '')
227
+ search = null;
228
+ else if (trimmed.length > 200)
229
+ issues.push({ field: 'q', message: 'Arama en çok 200 karakter olabilir.' });
230
+ else if (!entity.fields.some((f) => CONTAINS_TYPES.has(f.type)))
231
+ issues.push({ field: 'q', message: 'Bu kayıtlarda metin araması yapılamaz.' });
232
+ else
233
+ search = trimmed;
234
+ }
235
+ return { params: out, search, issues };
236
+ }
237
+ export function parseListQuery(entity, raw, defaults) {
238
+ const dialect = translateTableDialect(entity, raw);
239
+ const params = dialect.params;
240
+ const issues = [...dialect.issues];
241
+ const where = ['"deleted_at" IS NULL'];
242
+ const values = [];
243
+ let page = 1;
244
+ let size = defaults.pageSize;
245
+ let orderBy = '"created_at" DESC, "id" DESC';
246
+ let lang = 'tr';
247
+ const fields = new Map([...entity.fields, ...SYSTEM_DATETIME, CREATED_BY_FIELD].map((f) => [f.name, f]));
248
+ const seen = new Set();
249
+ let filters = 0;
250
+ for (const key of params.keys()) {
251
+ if (seen.has(key))
252
+ continue;
253
+ seen.add(key);
254
+ const all = params.getAll(key);
255
+ if (all.length > 1) {
256
+ issues.push({ field: key, message: `"${key}" sorgu parametresi bir kez verilebilir.` });
257
+ continue;
258
+ }
259
+ const raw = all[0];
260
+ if (key === '_page' || key === '_size') {
261
+ const n = /^\d{1,6}$/.test(raw) ? Number(raw) : NaN;
262
+ const max = key === '_page' ? 100_000 : CRUD_MAX_PAGE_SIZE;
263
+ if (!Number.isInteger(n) || n < 1 || n > max)
264
+ issues.push({ field: key, message: `"${key}" 1 ile ${max} arasında bir tamsayı olmalı.` });
265
+ else if (key === '_page')
266
+ page = n;
267
+ else
268
+ size = n;
269
+ continue;
270
+ }
271
+ if (key === '_lang') {
272
+ if (raw !== 'tr' && raw !== 'en')
273
+ issues.push({ field: key, message: '"_lang" tr ya da en olmalı.' });
274
+ else
275
+ lang = raw;
276
+ continue;
277
+ }
278
+ if (key === '_sort') {
279
+ const desc = raw.startsWith('-');
280
+ const name = desc ? raw.slice(1) : raw;
281
+ const f = fields.get(name);
282
+ if (!f || f.type === 'longText' || f.name === 'created_by')
283
+ issues.push({ field: key, message: `"${name}" alanına göre sıralanamaz.` });
284
+ else
285
+ orderBy = `${q(f.column)} ${desc ? 'DESC' : 'ASC'}, "id" ${desc ? 'DESC' : 'ASC'}`;
286
+ continue;
287
+ }
288
+ const dot = key.lastIndexOf('.');
289
+ const name = dot < 0 ? key : key.slice(0, dot);
290
+ const op = dot < 0 ? 'eq' : key.slice(dot + 1);
291
+ const f = fields.get(name);
292
+ if (!f || !['eq', 'contains', 'from', 'to'].includes(op)) {
293
+ issues.push({ field: key, message: `Bilinmeyen sorgu parametresi "${key}".` });
294
+ continue;
295
+ }
296
+ if (++filters > CRUD_MAX_FILTERS) {
297
+ issues.push({ field: key, message: `En çok ${CRUD_MAX_FILTERS} süzgeç verilebilir.` });
298
+ continue;
299
+ }
300
+ if (op === 'contains') {
301
+ if (!CONTAINS_TYPES.has(f.type))
302
+ issues.push({ field: key, message: `"${f.title.tr}" alanında "içerir" süzgeci kullanılamaz.` });
303
+ else if (raw === '' || raw.length > 200)
304
+ issues.push({ field: key, message: `"${f.title.tr}" araması 1-200 karakter olmalı.` });
305
+ else {
306
+ where.push(containsClause(f));
307
+ values.push(foldSearchText(raw));
308
+ }
309
+ continue;
310
+ }
311
+ if ((op === 'from' || op === 'to') && !RANGE_TYPES.has(f.type)) {
312
+ issues.push({ field: key, message: `"${f.title.tr}" alanında aralık süzgeci kullanılamaz.` });
313
+ continue;
314
+ }
315
+ if (op === 'eq' && f.type === 'longText') {
316
+ issues.push({ field: key, message: `"${f.title.tr}" alanında eşitlik süzgeci kullanılamaz; "${name}.contains" kullanın.` });
317
+ continue;
318
+ }
319
+ const c = coerceFilter(f, raw);
320
+ if (!c.ok || c.value === null) {
321
+ issues.push({ field: key, message: c.ok ? `"${f.title.tr}" süzgeci boş olamaz.` : c.message });
322
+ continue;
323
+ }
324
+ where.push(`${q(f.column)} ${op === 'eq' ? '=' : op === 'from' ? '>=' : '<='} ?`);
325
+ values.push(c.value);
326
+ }
327
+ if (dialect.search !== null && issues.length === 0) {
328
+ // `q`: metin alanlarının herhangi birinde içerir — her alan için ayrı bağlı parametre (değer SQL'e girmez).
329
+ const textFields = entity.fields.filter((f) => CONTAINS_TYPES.has(f.type));
330
+ where.push(`(${textFields.map(containsClause).join(' OR ')})`);
331
+ const folded = foldSearchText(dialect.search);
332
+ for (let i = 0; i < textFields.length; i++)
333
+ values.push(folded);
334
+ }
335
+ if (issues.length > 0)
336
+ throw new DataValidationError(400, 'ONE-SDK-E-104', 'Sorgu parametreleri geçersiz.', issues);
337
+ return { where, params: values, orderBy, page, size, lang };
338
+ }
339
+ function selectColumns(entity) {
340
+ return ['id', 'created_at', 'created_by', 'updated_at', 'updated_by', ...entity.fields.map((f) => f.column)].map(q).join(', ');
341
+ }
342
+ function rowToRecord(entity, row) {
343
+ const out = {
344
+ id: row.id,
345
+ created_at: row.created_at,
346
+ created_by: row.created_by,
347
+ updated_at: row.updated_at,
348
+ updated_by: row.updated_by,
349
+ };
350
+ for (const f of entity.fields)
351
+ out[f.name] = outputValue(f, row[f.column]);
352
+ return out;
353
+ }
354
+ const bind = (stmt, params) => (params.length > 0 ? stmt.bind(...params) : stmt);
355
+ /** Yazılacak kolonlar: alan kolonu + (metin alanında) arama gölgesi — gölge değerden katlanarak üretilir (S15). */
356
+ function writeColumns(values) {
357
+ const out = [];
358
+ for (const [f, v] of values) {
359
+ out.push([f.column, v]);
360
+ if (f.foldColumn !== undefined)
361
+ out.push([f.foldColumn, typeof v === 'string' ? foldSearchText(v) : null]);
362
+ }
363
+ return out;
364
+ }
365
+ /** Benzersizlik ihlali iletisinden (`UNIQUE constraint failed: <tablo>.<kolon>`) alanı bulur. */
366
+ export function uniqueViolationField(entity, error) {
367
+ const message = error instanceof Error ? error.message : String(error);
368
+ if (!/UNIQUE constraint failed/i.test(message))
369
+ return undefined;
370
+ const m = /UNIQUE constraint failed:\s*"?([a-z0-9_]+)"?\."?([a-z0-9_]+)"?/i.exec(message);
371
+ return (m && m[1] === entity.name && entity.fields.find((f) => f.column === m[2])) || null;
372
+ }
373
+ /** Model + varlık için uç işleyicisi. Model ilk kullanımda doğrulanır (geçersizse her istek 500 `ONE-SDK-E-100`). */
374
+ export function createCrudHandler(options) {
375
+ const basePath = (options.basePath ?? '/api').replace(/\/+$/, '');
376
+ if (!/^(\/[a-z0-9_-]+)*$/i.test(basePath))
377
+ throw new OneError(500, 'ONE-SDK-E-100', `basePath geçersiz: ${options.basePath}`);
378
+ if (options.records !== 'shared' && options.records !== 'own')
379
+ throw new OneError(500, 'ONE-SDK-E-100', '"records" "shared" ya da "own" olmalı (kayıtları kim görecek?).');
380
+ const pageSize = options.pageSize ?? 50;
381
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > CRUD_MAX_PAGE_SIZE)
382
+ throw new OneError(500, 'ONE-SDK-E-100', `pageSize 1 ile ${CRUD_MAX_PAGE_SIZE} arasında olmalı.`);
383
+ const exportLimit = Math.min(options.exportLimit ?? 10_000, 50_000);
384
+ const timeZone = options.timeZone ?? DEFAULT_TIME_ZONE;
385
+ const path = `${basePath}/${options.entity}`;
386
+ let entityCache;
387
+ const entityOf = () => {
388
+ if (entityCache)
389
+ return entityCache;
390
+ const model = loadModel(options.model);
391
+ const entity = model.entities.find((e) => e.name === options.entity);
392
+ if (!entity)
393
+ throw new OneError(500, 'ONE-SDK-E-100', `Veri modelinde "${options.entity}" varlığı yok.`);
394
+ entityCache = entity;
395
+ return entity;
396
+ };
397
+ const ownerClause = (user) => options.records === 'own' ? { sql: ['"created_by" = ?'], params: [user.sub] } : { sql: [], params: [] };
398
+ async function findRow(db, entity, id, owner) {
399
+ const where = ['"id" = ?', '"deleted_at" IS NULL', ...owner.sql].join(' AND ');
400
+ return bind(db.prepare(`SELECT ${selectColumns(entity)} FROM ${q(entity.name)} WHERE ${where} LIMIT 1`), [id, ...owner.params]).first();
401
+ }
402
+ async function writeOrConflict(entity, run) {
403
+ try {
404
+ await run();
405
+ }
406
+ catch (e) {
407
+ const f = uniqueViolationField(entity, e);
408
+ if (f === undefined)
409
+ throw e;
410
+ const field = f?.name ?? '';
411
+ throw new DataValidationError(409, 'ONE-SDK-E-103', 'Bu değer başka bir kayıtta kullanılıyor.', [
412
+ { field, message: f ? `"${f.title.tr}" değeri başka bir kayıtta kullanılıyor.` : 'Bu değer başka bir kayıtta kullanılıyor.' },
413
+ ]);
414
+ }
415
+ }
416
+ function validateBody(entity, body, mode) {
417
+ const issues = [];
418
+ const byName = new Map(entity.fields.map((f) => [f.name, f]));
419
+ for (const key of Object.keys(body)) {
420
+ if (SYSTEM_SET.has(key))
421
+ issues.push({ field: key, message: `"${key}" platform tarafından yazılır; gövdede gönderilemez.` });
422
+ else if (!byName.has(key))
423
+ issues.push({ field: key, message: `"${key}" bu kayıtta bulunmayan bir alan.` });
424
+ }
425
+ const out = new Map();
426
+ for (const f of entity.fields) {
427
+ const present = Object.prototype.hasOwnProperty.call(body, f.name);
428
+ if (!present && mode === 'update')
429
+ continue;
430
+ let value = null;
431
+ if (present) {
432
+ const c = coerceInput(f, body[f.name]);
433
+ if (!c.ok) {
434
+ issues.push({ field: f.name, message: c.message });
435
+ continue;
436
+ }
437
+ value = c.value;
438
+ }
439
+ if (value === null && mode === 'create')
440
+ value = defaultDbValue(f) ?? null;
441
+ if (value === null && f.required) {
442
+ issues.push({ field: f.name, message: `"${f.title.tr}" zorunlu.` });
443
+ continue;
444
+ }
445
+ out.set(f, value);
446
+ }
447
+ if (mode === 'update' && issues.length === 0 && out.size === 0)
448
+ issues.push({ field: '', message: 'Güncellenecek alan gönderilmedi.' });
449
+ if (issues.length > 0)
450
+ throw new DataValidationError(400, 'ONE-SDK-E-101', 'Kayıt doğrulanamadı.', issues);
451
+ return out;
452
+ }
453
+ function guardWrite(ctx) {
454
+ const user = requireUser(ctx);
455
+ if (isActingSession(ctx) && options.actingSessions !== 'write')
456
+ throw new OneError(403, 'ONE-SDK-E-106', 'Vekalet ya da Yerine Görüntüle oturumunda bu uygulamada kayıt değiştirilemez; yalnız görüntüleyebilirsiniz.');
457
+ if (!allowed(ctx, options.canWrite))
458
+ throw new OneError(403, 'ONE-SDK-E-107', 'Bu kaydı değiştirme yetkiniz yok.');
459
+ return user;
460
+ }
461
+ function guardRead(ctx) {
462
+ const user = requireUser(ctx);
463
+ if (!allowed(ctx, options.canRead))
464
+ throw new OneError(403, 'ONE-SDK-E-107', 'Bu kayıtları görme yetkiniz yok.');
465
+ return user;
466
+ }
467
+ async function listRows(db, entity, lq, owner, limit, offset) {
468
+ const where = [...lq.where, ...owner.sql].join(' AND ');
469
+ const params = [...lq.params, ...owner.params];
470
+ const count = bind(db.prepare(`SELECT COUNT(*) AS "n" FROM ${q(entity.name)} WHERE ${where}`), params);
471
+ const list = bind(db.prepare(`SELECT ${selectColumns(entity)} FROM ${q(entity.name)} WHERE ${where} ORDER BY ${lq.orderBy} LIMIT ? OFFSET ?`), [
472
+ ...params,
473
+ limit,
474
+ offset,
475
+ ]);
476
+ const [countResult, listResult] = await db.batch([count, list]);
477
+ const total = Number(countResult?.results[0]?.n ?? 0);
478
+ return { total, rows: (listResult?.results ?? []).map((r) => rowToRecord(entity, r)) };
479
+ }
480
+ function exportColumns(entity) {
481
+ const cols = entity.fields.map((f) => {
482
+ const type = f.type === 'number' || f.type === 'money' || f.type === 'date' || f.type === 'datetime' || f.type === 'boolean' ? f.type : 'text';
483
+ const col = { key: f.name, title: f.title, type };
484
+ if (f.type === 'choice')
485
+ col.labels = Object.fromEntries(f.choices.map((c) => [c.value, c.title]));
486
+ return col;
487
+ });
488
+ cols.push({ key: 'created_at', title: { tr: 'Oluşturulma', en: 'Created' }, type: 'datetime' });
489
+ return cols;
490
+ }
491
+ async function handle(request, db, given) {
492
+ const url = new URL(request.url);
493
+ let rest = url.pathname === path ? '' : url.pathname.startsWith(`${path}/`) ? url.pathname.slice(path.length + 1) : null;
494
+ if (rest === null)
495
+ return null;
496
+ // Hazır liste bileşeni (`<one-table export>`) dışa aktarmayı listeyle aynı adreste `?format=xlsx|csv` ile ister.
497
+ const format = rest === '' && !fieldNameIn(options.model, options.entity, 'format') ? url.searchParams.get('format') : null;
498
+ let ctx = given;
499
+ try {
500
+ ctx ??= readContext(request.headers);
501
+ const entity = entityOf();
502
+ const method = request.method.toUpperCase();
503
+ if (format !== null) {
504
+ if (url.searchParams.getAll('format').length > 1 || (format !== 'xlsx' && format !== 'csv'))
505
+ throw new DataValidationError(400, 'ONE-SDK-E-104', 'Sorgu parametreleri geçersiz.', [{ field: 'format', message: '"format" xlsx ya da csv olmalı.' }]);
506
+ url.searchParams.delete('format');
507
+ rest = `export.${format}`;
508
+ }
509
+ if (rest === 'export.csv' || rest === 'export.xlsx' || rest === 'report.html') {
510
+ if (method !== 'GET' && method !== 'HEAD')
511
+ throw new OneError(405, 'ONE-SDK-E-105', 'Bu adreste yalnız okuma yapılır (GET).');
512
+ const user = guardRead(ctx);
513
+ const lq = parseListQuery(entity, url.searchParams, { pageSize });
514
+ const limit = rest === 'report.html' ? Math.min(exportLimit, CRUD_REPORT_MAX_ROWS) : exportLimit;
515
+ const { total, rows } = await listRows(db, entity, lq, ownerClause(user), limit, 0);
516
+ if (total > limit)
517
+ throw new OneError(400, 'ONE-SDK-E-110', `${total.toLocaleString('tr-TR')} kayıt var; en çok ${limit.toLocaleString('tr-TR')} kayıt aktarılabilir. Süzgeci daraltın.`);
518
+ const columns = exportColumns(entity);
519
+ const title = entity.title[lq.lang];
520
+ const today = new Date().toISOString().slice(0, 10);
521
+ const fileName = `${title} ${formatDate(today, lq.lang)}`;
522
+ logEvent(ctx, 'info', 'data.export', { entity: entity.name, format: rest, rows: rows.length });
523
+ if (rest === 'export.csv')
524
+ return exportResponse(toCsv(rows, columns, { lang: lq.lang, timeZone }), { fileName, format: 'csv' });
525
+ if (rest === 'export.xlsx')
526
+ return exportResponse(toXlsx(rows, columns, { lang: lq.lang, timeZone, sheetName: title }), { fileName, format: 'xlsx' });
527
+ const totals = columns.filter((c) => c.type === 'money').map((c) => c.key);
528
+ return reportResponse(renderPrintableReport({ title, columns, rows, totals, lang: lq.lang, timeZone }));
529
+ }
530
+ if (rest === '') {
531
+ if (method === 'GET' || method === 'HEAD') {
532
+ const user = guardRead(ctx);
533
+ const lq = parseListQuery(entity, url.searchParams, { pageSize });
534
+ const { total, rows } = await listRows(db, entity, lq, ownerClause(user), lq.size, (lq.page - 1) * lq.size);
535
+ // `nextCursor`: hazır liste bileşeninin "sonraki sayfa" imleci (bu uçlarda sayfa numarası); son sayfada null.
536
+ return json({ items: rows, page: lq.page, pageSize: lq.size, total, nextCursor: lq.page * lq.size < total ? String(lq.page + 1) : null });
537
+ }
538
+ if (method === 'POST') {
539
+ const user = guardWrite(ctx);
540
+ const values = validateBody(entity, await readJsonBody(request), 'create');
541
+ const id = crypto.randomUUID();
542
+ const now = new Date().toISOString();
543
+ const written = writeColumns(values);
544
+ const cols = ['id', 'created_at', 'created_by', 'updated_at', 'updated_by', ...written.map(([c]) => c)];
545
+ const params = [id, now, user.sub, now, user.sub, ...written.map(([, v]) => v)];
546
+ await writeOrConflict(entity, () => bind(db.prepare(`INSERT INTO ${q(entity.name)} (${cols.map(q).join(', ')}) VALUES (${cols.map(() => '?').join(', ')})`), params).run());
547
+ if (isActingSession(ctx))
548
+ logEvent(ctx, 'info', 'data.acting-write', { entity: entity.name, id, actor: ctx.actor?.sub ?? null });
549
+ const row = await findRow(db, entity, id, { sql: [], params: [] });
550
+ return json(row ? rowToRecord(entity, row) : { id }, 201, { location: `${path}/${id}` });
551
+ }
552
+ throw new OneError(405, 'ONE-SDK-E-105', 'Bu adreste bu yöntem kullanılamaz (GET, HEAD, POST).');
553
+ }
554
+ if (!UUID_RE.test(rest))
555
+ throw new OneError(404, 'ONE-SDK-E-102', 'Kayıt bulunamadı.');
556
+ const id = rest;
557
+ if (method === 'GET' || method === 'HEAD') {
558
+ const user = guardRead(ctx);
559
+ const row = await findRow(db, entity, id, ownerClause(user));
560
+ if (!row)
561
+ throw new OneError(404, 'ONE-SDK-E-102', 'Kayıt bulunamadı.');
562
+ return json(rowToRecord(entity, row));
563
+ }
564
+ if (method === 'PATCH' || method === 'DELETE') {
565
+ const user = guardWrite(ctx);
566
+ const owner = ownerClause(user);
567
+ const values = method === 'PATCH' ? validateBody(entity, await readJsonBody(request), 'update') : null;
568
+ if (!(await findRow(db, entity, id, owner)))
569
+ throw new OneError(404, 'ONE-SDK-E-102', 'Kayıt bulunamadı.');
570
+ const now = new Date().toISOString();
571
+ const guard = ['"id" = ?', '"deleted_at" IS NULL', ...owner.sql].join(' AND ');
572
+ if (values) {
573
+ const written = writeColumns(values);
574
+ const sets = [...written.map(([c]) => `${q(c)} = ?`), '"updated_at" = ?', '"updated_by" = ?'];
575
+ await writeOrConflict(entity, () => bind(db.prepare(`UPDATE ${q(entity.name)} SET ${sets.join(', ')} WHERE ${guard}`), [
576
+ ...written.map(([, v]) => v),
577
+ now,
578
+ user.sub,
579
+ id,
580
+ ...owner.params,
581
+ ]).run());
582
+ if (isActingSession(ctx))
583
+ logEvent(ctx, 'info', 'data.acting-write', { entity: entity.name, id, actor: ctx.actor?.sub ?? null });
584
+ const row = await findRow(db, entity, id, owner);
585
+ if (!row)
586
+ throw new OneError(404, 'ONE-SDK-E-102', 'Kayıt bulunamadı.');
587
+ return json(rowToRecord(entity, row));
588
+ }
589
+ if (entity.softDelete)
590
+ await bind(db.prepare(`UPDATE ${q(entity.name)} SET "deleted_at" = ?, "updated_at" = ?, "updated_by" = ? WHERE ${guard}`), [
591
+ now,
592
+ now,
593
+ user.sub,
594
+ id,
595
+ ...owner.params,
596
+ ]).run();
597
+ else
598
+ await bind(db.prepare(`DELETE FROM ${q(entity.name)} WHERE ${guard}`), [id, ...owner.params]).run();
599
+ if (isActingSession(ctx))
600
+ logEvent(ctx, 'info', 'data.acting-write', { entity: entity.name, id, actor: ctx.actor?.sub ?? null, deleted: true });
601
+ return new Response(null, { status: 204 });
602
+ }
603
+ throw new OneError(405, 'ONE-SDK-E-105', 'Bu adreste bu yöntem kullanılamaz (GET, HEAD, PATCH, DELETE).');
604
+ }
605
+ catch (e) {
606
+ if (e instanceof OneError)
607
+ return problem(e);
608
+ // Bilinmeyen hata: iç ileti yanıta sızmaz, olay günlüğüne yazılır (D-45).
609
+ return problemResponse(e, ctx);
610
+ }
611
+ }
612
+ return { path, handle };
613
+ }
614
+ //# sourceMappingURL=crud.js.map