firebase-contract 0.1.0-rc.0
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 +560 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/lib/_internal/compiler/compiler.d.ts +71 -0
- package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -0
- package/dist/lib/_internal/compiler/compiler.js +104 -0
- package/dist/lib/_internal/compiler/index.d.ts +3 -0
- package/dist/lib/_internal/compiler/index.d.ts.map +1 -0
- package/dist/lib/_internal/compiler/index.js +1 -0
- package/dist/lib/_internal/diagnostics.d.ts +29 -0
- package/dist/lib/_internal/diagnostics.d.ts.map +1 -0
- package/dist/lib/_internal/diagnostics.js +26 -0
- package/dist/lib/_internal/format-diagnostics.d.ts +8 -0
- package/dist/lib/_internal/format-diagnostics.d.ts.map +1 -0
- package/dist/lib/_internal/format-diagnostics.js +10 -0
- package/dist/lib/_internal/generators/api/api-dto-generator.d.ts +8 -0
- package/dist/lib/_internal/generators/api/api-dto-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/api/api-dto-generator.js +134 -0
- package/dist/lib/_internal/generators/api/api-types-generator.d.ts +8 -0
- package/dist/lib/_internal/generators/api/api-types-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/api/api-types-generator.js +105 -0
- package/dist/lib/_internal/generators/api/api-validation-generator.d.ts +7 -0
- package/dist/lib/_internal/generators/api/api-validation-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/api/api-validation-generator.js +73 -0
- package/dist/lib/_internal/generators/api/task-payload-generator.d.ts +9 -0
- package/dist/lib/_internal/generators/api/task-payload-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/api/task-payload-generator.js +96 -0
- package/dist/lib/_internal/generators/config/config-generator.d.ts +9 -0
- package/dist/lib/_internal/generators/config/config-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/config/config-generator.js +71 -0
- package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts +8 -0
- package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/data-connect/adapter-generator.js +142 -0
- package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts +15 -0
- package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/data-connect/graphql-generator.js +209 -0
- package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts +18 -0
- package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/data-connect/operations-generator.js +539 -0
- package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts +11 -0
- package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/firestore/firestore-projection-generator.js +112 -0
- package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts +11 -0
- package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/firestore/firestore-split-generator.js +375 -0
- package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts +12 -0
- package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/firestore/firestore-type-generator.js +81 -0
- package/dist/lib/_internal/generators/generator.d.ts +23 -0
- package/dist/lib/_internal/generators/generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/generator.js +1 -0
- package/dist/lib/_internal/generators/id/id-codec-generator.d.ts +16 -0
- package/dist/lib/_internal/generators/id/id-codec-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/id/id-codec-generator.js +140 -0
- package/dist/lib/_internal/generators/index.d.ts +31 -0
- package/dist/lib/_internal/generators/index.d.ts.map +1 -0
- package/dist/lib/_internal/generators/index.js +64 -0
- package/dist/lib/_internal/generators/registry.d.ts +15 -0
- package/dist/lib/_internal/generators/registry.d.ts.map +1 -0
- package/dist/lib/_internal/generators/registry.js +24 -0
- package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts +9 -0
- package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/sql/sql-migration-generator.js +44 -0
- package/dist/lib/_internal/generators/support/constraints.d.ts +8 -0
- package/dist/lib/_internal/generators/support/constraints.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/constraints.js +39 -0
- package/dist/lib/_internal/generators/support/header.d.ts +13 -0
- package/dist/lib/_internal/generators/support/header.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/header.js +17 -0
- package/dist/lib/_internal/generators/support/naming.d.ts +10 -0
- package/dist/lib/_internal/generators/support/naming.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/naming.js +30 -0
- package/dist/lib/_internal/generators/support/relations.d.ts +12 -0
- package/dist/lib/_internal/generators/support/relations.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/relations.js +11 -0
- package/dist/lib/_internal/generators/support/split.d.ts +34 -0
- package/dist/lib/_internal/generators/support/split.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/split.js +75 -0
- package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts +18 -0
- package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/typescript/typescript-generator.js +99 -0
- package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts +10 -0
- package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +175 -0
- package/dist/lib/_internal/generators/union/union-generator.d.ts +8 -0
- package/dist/lib/_internal/generators/union/union-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/union/union-generator.js +37 -0
- package/dist/lib/_internal/generators/zod/zod-generator.d.ts +7 -0
- package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/zod/zod-generator.js +68 -0
- package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts +9 -0
- package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/zod/zod-split-generator.js +136 -0
- package/dist/lib/_internal/ir/build-ir.d.ts +18 -0
- package/dist/lib/_internal/ir/build-ir.d.ts.map +1 -0
- package/dist/lib/_internal/ir/build-ir.js +430 -0
- package/dist/lib/_internal/ir/ir.d.ts +393 -0
- package/dist/lib/_internal/ir/ir.d.ts.map +1 -0
- package/dist/lib/_internal/ir/ir.js +22 -0
- package/dist/lib/_internal/parser/parse.d.ts +9 -0
- package/dist/lib/_internal/parser/parse.d.ts.map +1 -0
- package/dist/lib/_internal/parser/parse.js +682 -0
- package/dist/lib/_internal/parser/raw-document.d.ts +230 -0
- package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -0
- package/dist/lib/_internal/parser/raw-document.js +6 -0
- package/dist/lib/_internal/resolver/import-resolver.d.ts +25 -0
- package/dist/lib/_internal/resolver/import-resolver.d.ts.map +1 -0
- package/dist/lib/_internal/resolver/import-resolver.js +73 -0
- package/dist/lib/_internal/resolver/module-loader.d.ts +25 -0
- package/dist/lib/_internal/resolver/module-loader.d.ts.map +1 -0
- package/dist/lib/_internal/resolver/module-loader.js +62 -0
- package/dist/lib/_internal/runtime/data-connect-adapter.d.ts +25 -0
- package/dist/lib/_internal/runtime/data-connect-adapter.d.ts.map +1 -0
- package/dist/lib/_internal/runtime/data-connect-adapter.js +24 -0
- package/dist/lib/_internal/runtime/index.d.ts +3 -0
- package/dist/lib/_internal/runtime/index.d.ts.map +1 -0
- package/dist/lib/_internal/runtime/index.js +1 -0
- package/dist/lib/_internal/validation/rules.d.ts +32 -0
- package/dist/lib/_internal/validation/rules.d.ts.map +1 -0
- package/dist/lib/_internal/validation/rules.js +282 -0
- package/dist/lib/_internal/validation/validate.d.ts +9 -0
- package/dist/lib/_internal/validation/validate.d.ts.map +1 -0
- package/dist/lib/_internal/validation/validate.js +6 -0
- package/dist/lib/cli/commands/generate.d.ts +3 -0
- package/dist/lib/cli/commands/generate.d.ts.map +1 -0
- package/dist/lib/cli/commands/generate.js +66 -0
- package/dist/lib/cli/commands/index.d.ts +4 -0
- package/dist/lib/cli/commands/index.d.ts.map +1 -0
- package/dist/lib/cli/commands/index.js +11 -0
- package/dist/lib/cli/commands/init.d.ts +3 -0
- package/dist/lib/cli/commands/init.d.ts.map +1 -0
- package/dist/lib/cli/commands/init.js +72 -0
- package/dist/lib/cli/commands/inspect.d.ts +3 -0
- package/dist/lib/cli/commands/inspect.d.ts.map +1 -0
- package/dist/lib/cli/commands/inspect.js +23 -0
- package/dist/lib/cli/commands/validate.d.ts +3 -0
- package/dist/lib/cli/commands/validate.d.ts.map +1 -0
- package/dist/lib/cli/commands/validate.js +23 -0
- package/dist/lib/cli/config.d.ts +12 -0
- package/dist/lib/cli/config.d.ts.map +1 -0
- package/dist/lib/cli/config.js +20 -0
- package/dist/lib/cli/index.d.ts +3 -0
- package/dist/lib/cli/index.d.ts.map +1 -0
- package/dist/lib/cli/index.js +13 -0
- package/dist/lib/cli/version.d.ts +3 -0
- package/dist/lib/cli/version.d.ts.map +1 -0
- package/dist/lib/cli/version.js +15 -0
- package/dist/lib/modules/runtime.d.ts +2 -0
- package/dist/lib/modules/runtime.d.ts.map +1 -0
- package/dist/lib/modules/runtime.js +3 -0
- package/package.json +60 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firestore-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/firestore/firestore-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAwO5E;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,QAAO,SAqI/C,CAAA"}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { findEnum, findModel } from '../../ir/ir.js';
|
|
2
|
+
import { zodConstraints } from '../support/constraints.js';
|
|
3
|
+
import { headerBlocks } from '../support/header.js';
|
|
4
|
+
import { camelCase, constantCase, pluralize, singleQuote } from '../support/naming.js';
|
|
5
|
+
import { isRelation, relationFkName } from '../support/relations.js';
|
|
6
|
+
import { collectDeps, kebabCase, tableNameOf } from '../support/split.js';
|
|
7
|
+
// Projection-level scalars: timestamps become dates, ids/relations become
|
|
8
|
+
// resolved (hashids-encoded) string ids.
|
|
9
|
+
const SCALAR_FS = {
|
|
10
|
+
string: 'z.string()',
|
|
11
|
+
int: 'z.number()',
|
|
12
|
+
int64: 'z.number()',
|
|
13
|
+
float: 'z.number()',
|
|
14
|
+
boolean: 'z.boolean()',
|
|
15
|
+
timestamp: 'z.date()',
|
|
16
|
+
date: 'z.date()',
|
|
17
|
+
json: 'z.unknown()',
|
|
18
|
+
id: 'z.string()',
|
|
19
|
+
};
|
|
20
|
+
// Embedded value objects keep the stricter Zod chains (`.int()` etc.).
|
|
21
|
+
const SCALAR_EMBEDDED = {
|
|
22
|
+
...SCALAR_FS,
|
|
23
|
+
int: 'z.number().int()',
|
|
24
|
+
int64: 'z.number().int()',
|
|
25
|
+
};
|
|
26
|
+
const META_FILE = `import { z } from 'zod'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cloud Task 整合性管理用の operation 識別子メタ。
|
|
30
|
+
* 整合性回復 (reconciliation) 処理の最新世代を表す。
|
|
31
|
+
*/
|
|
32
|
+
export const _Meta_OperationSchema = z.object({
|
|
33
|
+
/** opId (operation id) */
|
|
34
|
+
id: z.string(),
|
|
35
|
+
/** 整合性回復処理の識別子 */
|
|
36
|
+
identifierId: z.string(),
|
|
37
|
+
/** unixtime (最終更新) */
|
|
38
|
+
updatedAt: z.number(),
|
|
39
|
+
})
|
|
40
|
+
export type _Meta_Operation = z.infer<typeof _Meta_OperationSchema>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 全 Firestore document に付ける整合性管理メタ。
|
|
44
|
+
*
|
|
45
|
+
* - \`scope\`: app hosting 経由で set される unixtime
|
|
46
|
+
* - \`sys\`: cloud functions / cloud task 経由で set される unixtime
|
|
47
|
+
* - \`op\`: cloud task で set される operation メタ
|
|
48
|
+
*/
|
|
49
|
+
export const _Meta_Schema = z.object({
|
|
50
|
+
scope: z.number().nullable(),
|
|
51
|
+
sys: z.number().nullable(),
|
|
52
|
+
op: _Meta_OperationSchema.nullable(),
|
|
53
|
+
})
|
|
54
|
+
export type _Meta_ = z.infer<typeof _Meta_Schema>
|
|
55
|
+
`;
|
|
56
|
+
const lineComments = (description, indent) => description ? description.split('\n').map(line => `${indent}// ${line}`.trimEnd()) : [];
|
|
57
|
+
const jsdoc = (description, indent) => {
|
|
58
|
+
if (!description)
|
|
59
|
+
return [];
|
|
60
|
+
const lines = description.split('\n');
|
|
61
|
+
if (lines.length === 1)
|
|
62
|
+
return [`${indent}/** ${lines[0]} */`];
|
|
63
|
+
return [`${indent}/**`, ...lines.map(line => `${indent} * ${line}`.trimEnd()), `${indent} */`];
|
|
64
|
+
};
|
|
65
|
+
const fsEnumName = (irEnum) => irEnum.fsName ?? irEnum.name;
|
|
66
|
+
const fsModelName = (ir, name) => findModel(ir, name)?.fsName ?? name;
|
|
67
|
+
/**
|
|
68
|
+
* Frozen const object + Key/value types + Zod enum referencing the const —
|
|
69
|
+
* the hand-maintained representation this generator standardizes on.
|
|
70
|
+
*/
|
|
71
|
+
const renderFsEnum = (irEnum) => {
|
|
72
|
+
const name = fsEnumName(irEnum);
|
|
73
|
+
const constName = constantCase(name);
|
|
74
|
+
const keyOf = (value) => irEnum.valueKeys?.[value] ?? constantCase(value);
|
|
75
|
+
const entries = irEnum.values.map(value => ` ${keyOf(value)}: ${singleQuote(value)},`);
|
|
76
|
+
const schemaValues = irEnum.values.map(value => ` ${constName}.${keyOf(value)},`);
|
|
77
|
+
return [
|
|
78
|
+
...lineComments(irEnum.fsDescription ?? irEnum.description, ''),
|
|
79
|
+
`export const ${constName} = Object.freeze({`,
|
|
80
|
+
...entries,
|
|
81
|
+
`} as const)`,
|
|
82
|
+
`export type ${name}Key = keyof typeof ${constName}`,
|
|
83
|
+
`export type ${name} = (typeof ${constName})[${name}Key]`,
|
|
84
|
+
`export const ${name}Schema = z.enum([`,
|
|
85
|
+
...schemaValues,
|
|
86
|
+
`])`,
|
|
87
|
+
].join('\n');
|
|
88
|
+
};
|
|
89
|
+
/** Zod chain for one field of an embedded value object. */
|
|
90
|
+
const embeddedFieldSchema = (ir, field) => {
|
|
91
|
+
let base;
|
|
92
|
+
if (field.type.kind === 'scalar')
|
|
93
|
+
base = SCALAR_EMBEDDED[field.type.name];
|
|
94
|
+
else if (field.type.kind === 'enum') {
|
|
95
|
+
const irEnum = findEnum(ir, field.type.name);
|
|
96
|
+
base = irEnum ? `${fsEnumName(irEnum)}Schema` : 'z.string()';
|
|
97
|
+
}
|
|
98
|
+
else if (field.type.kind === 'model')
|
|
99
|
+
base = `${fsModelName(ir, field.type.name)}Schema`;
|
|
100
|
+
else
|
|
101
|
+
base = 'z.unknown()';
|
|
102
|
+
let schema = field.list ? `z.array(${base})${zodConstraints(field)}` : `${base}${zodConstraints(field)}`;
|
|
103
|
+
if (field.default !== undefined)
|
|
104
|
+
schema = `${schema}.default(${JSON.stringify(field.default)})`;
|
|
105
|
+
if (field.nullable)
|
|
106
|
+
schema = `${schema}.nullable()`;
|
|
107
|
+
if (field.optional)
|
|
108
|
+
schema = `${schema}.optional()`;
|
|
109
|
+
return schema;
|
|
110
|
+
};
|
|
111
|
+
const renderEmbeddedModel = (ir, model) => {
|
|
112
|
+
const lines = [];
|
|
113
|
+
const desc = model.description ?? '';
|
|
114
|
+
const name = model.fsName ?? model.name;
|
|
115
|
+
lines.push(...(desc.includes('\n') ? jsdoc(desc, '') : lineComments(desc || undefined, '')));
|
|
116
|
+
lines.push(`export const ${name}Schema = z.object({`);
|
|
117
|
+
for (const field of model.fields) {
|
|
118
|
+
lines.push(...jsdoc(field.description, ' '));
|
|
119
|
+
lines.push(` ${field.name}: ${embeddedFieldSchema(ir, field)},`);
|
|
120
|
+
}
|
|
121
|
+
lines.push('})');
|
|
122
|
+
lines.push(`export type ${name} = z.infer<typeof ${name}Schema>`);
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
};
|
|
125
|
+
// zod-split と同じ DC 側チェーン。FS 側と文字列一致するフィールドは
|
|
126
|
+
// `.pick()` で DC スキーマから流用できる。
|
|
127
|
+
const SCALAR_DC = {
|
|
128
|
+
string: 'z.string()',
|
|
129
|
+
int: 'z.number().int()',
|
|
130
|
+
int64: 'z.number().int()',
|
|
131
|
+
float: 'z.number()',
|
|
132
|
+
boolean: 'z.boolean()',
|
|
133
|
+
timestamp: 'z.string().datetime()',
|
|
134
|
+
date: 'z.string()',
|
|
135
|
+
json: 'z.unknown()',
|
|
136
|
+
id: 'z.string()',
|
|
137
|
+
};
|
|
138
|
+
const dcFieldChain = (ir, field) => {
|
|
139
|
+
const base = isRelation(field)
|
|
140
|
+
? SCALAR_DC[findModel(ir, field.type.kind === 'model' ? field.type.name : '')?.fields.find(f => f.isId)?.type?.name ?? 'string']
|
|
141
|
+
: field.type.kind === 'scalar'
|
|
142
|
+
? SCALAR_DC[field.type.name]
|
|
143
|
+
: field.type.kind === 'enum'
|
|
144
|
+
? `${field.type.name}Schema`
|
|
145
|
+
: 'z.unknown()';
|
|
146
|
+
let chain = field.list ? `z.array(${base})${zodConstraints(field)}` : `${base}${zodConstraints(field)}`;
|
|
147
|
+
if (field.nullable)
|
|
148
|
+
chain = `${chain}.nullable()`;
|
|
149
|
+
if (field.optional)
|
|
150
|
+
chain = `${chain}.optional()`;
|
|
151
|
+
return chain;
|
|
152
|
+
};
|
|
153
|
+
/** Zod chain for a base (picked) Data Connect field in the projection. */
|
|
154
|
+
const baseFieldSchema = (ir, field) => {
|
|
155
|
+
const name = isRelation(field) ? relationFkName(field) : field.name;
|
|
156
|
+
let base;
|
|
157
|
+
if (isRelation(field))
|
|
158
|
+
base = 'z.string()'; // resolved, encoded foreign-key id
|
|
159
|
+
else if (field.type.kind === 'enum') {
|
|
160
|
+
const irEnum = findEnum(ir, field.type.name);
|
|
161
|
+
base = irEnum ? `${fsEnumName(irEnum)}Schema` : 'z.string()';
|
|
162
|
+
}
|
|
163
|
+
else if (field.type.kind === 'scalar')
|
|
164
|
+
base = SCALAR_FS[field.type.name];
|
|
165
|
+
else if (field.type.kind === 'model')
|
|
166
|
+
base = `${fsModelName(ir, field.type.name)}Schema`;
|
|
167
|
+
else
|
|
168
|
+
base = 'z.unknown()';
|
|
169
|
+
let schema = field.list ? `z.array(${base})` : base;
|
|
170
|
+
if (field.optional)
|
|
171
|
+
schema = `${schema}.nullable()`;
|
|
172
|
+
return { name, schema };
|
|
173
|
+
};
|
|
174
|
+
/** Zod chain for a projection-specific (added/overriding) field. */
|
|
175
|
+
const overrideFieldSchema = (ir, field) => {
|
|
176
|
+
const name = isRelation(field) ? relationFkName(field) : field.name;
|
|
177
|
+
let base;
|
|
178
|
+
if (field.type.kind === 'enum') {
|
|
179
|
+
const irEnum = findEnum(ir, field.type.name);
|
|
180
|
+
base = irEnum ? `${fsEnumName(irEnum)}Schema` : 'z.string()';
|
|
181
|
+
}
|
|
182
|
+
else if (field.type.kind === 'scalar')
|
|
183
|
+
base = SCALAR_FS[field.type.name];
|
|
184
|
+
else if (field.type.kind === 'model')
|
|
185
|
+
base = `${fsModelName(ir, field.type.name)}Schema`;
|
|
186
|
+
else
|
|
187
|
+
base = 'z.unknown()';
|
|
188
|
+
let schema = field.list ? `z.array(${base})` : base;
|
|
189
|
+
if (field.default !== undefined)
|
|
190
|
+
schema = `${schema}.default(${JSON.stringify(field.default)})`;
|
|
191
|
+
if (field.nullable)
|
|
192
|
+
schema = `${schema}.nullable()`;
|
|
193
|
+
if (field.optional)
|
|
194
|
+
schema = `${schema}.optional()`;
|
|
195
|
+
return { name, schema };
|
|
196
|
+
};
|
|
197
|
+
const effectiveFields = (ir, doc) => {
|
|
198
|
+
const base = doc.from ? findModel(ir, doc.from) : undefined;
|
|
199
|
+
let baseFields = base?.fields ?? [];
|
|
200
|
+
if (doc.pick.length > 0) {
|
|
201
|
+
// Pick order is authoritative for the projected field order.
|
|
202
|
+
baseFields = doc.pick
|
|
203
|
+
.map(name => baseFields.find(field => field.name === name))
|
|
204
|
+
.filter((field) => Boolean(field));
|
|
205
|
+
}
|
|
206
|
+
else if (doc.omit.length > 0) {
|
|
207
|
+
baseFields = baseFields.filter(field => !doc.omit.includes(field.name));
|
|
208
|
+
}
|
|
209
|
+
// Insertion order = base fields, then added/overriding fields (override keeps position).
|
|
210
|
+
const entries = new Map();
|
|
211
|
+
const roots = [];
|
|
212
|
+
for (const field of baseFields) {
|
|
213
|
+
const { name, schema } = baseFieldSchema(ir, field);
|
|
214
|
+
const pickable = dcFieldChain(ir, field) === schema;
|
|
215
|
+
entries.set(name, { comments: [], entry: ` ${name}: ${schema},`, name, pickable });
|
|
216
|
+
roots.push(field);
|
|
217
|
+
}
|
|
218
|
+
for (const field of doc.fields) {
|
|
219
|
+
const { name, schema } = overrideFieldSchema(ir, field);
|
|
220
|
+
const comments = field.jsdoc ? jsdoc(field.description, ' ') : lineComments(field.description, ' ');
|
|
221
|
+
entries.set(name, { comments, entry: ` ${name}: ${schema},`, name, pickable: false });
|
|
222
|
+
roots.push(field);
|
|
223
|
+
}
|
|
224
|
+
return { rendered: [...entries.values()], roots };
|
|
225
|
+
};
|
|
226
|
+
const docModuleName = (doc) => camelCase(pluralize(doc.name));
|
|
227
|
+
/**
|
|
228
|
+
* Split variant of the Firestore projection generator, mirroring the
|
|
229
|
+
* hand-maintained `libs/types/src/schema/firestore/*` layout: one file per
|
|
230
|
+
* projection at `firestore/<collectionName>.ts`, FS-only enums (frozen consts)
|
|
231
|
+
* and embedded value objects co-located at their first referencing projection,
|
|
232
|
+
* the `_meta_` envelope under `firestore/_/`, and a `firestore.ts` barrel that
|
|
233
|
+
* also carries the `FIRESTORE_DATABASES` constants.
|
|
234
|
+
*/
|
|
235
|
+
export const createFirestoreSplitGenerator = () => ({
|
|
236
|
+
name: 'firestore-split',
|
|
237
|
+
description: 'Firestore projection Zod schemas, one file per collection (firestore/<name>.ts + firestore.ts barrel)',
|
|
238
|
+
generate(ir, context) {
|
|
239
|
+
if (ir.firestore.length === 0)
|
|
240
|
+
return [];
|
|
241
|
+
const files = [];
|
|
242
|
+
// Co-locate each referenced enum / embedded model at its first referencing doc.
|
|
243
|
+
const enumHome = new Map();
|
|
244
|
+
const modelHome = new Map();
|
|
245
|
+
const docDeps = new Map();
|
|
246
|
+
for (const doc of ir.firestore) {
|
|
247
|
+
const { roots } = effectiveFields(ir, doc);
|
|
248
|
+
const pseudo = { name: `__${doc.name}`, fields: roots, key: [], indexes: [] };
|
|
249
|
+
const collected = collectDeps(ir, [pseudo]);
|
|
250
|
+
const included = (doc.include ?? []).filter(name => !collected.models.includes(name));
|
|
251
|
+
const deps = { enums: collected.enums, models: [...included, ...collected.models] };
|
|
252
|
+
docDeps.set(doc.name, deps);
|
|
253
|
+
const moduleName = docModuleName(doc);
|
|
254
|
+
for (const name of deps.enums)
|
|
255
|
+
if (!enumHome.has(name))
|
|
256
|
+
enumHome.set(name, moduleName);
|
|
257
|
+
for (const name of deps.models)
|
|
258
|
+
if (!modelHome.has(name))
|
|
259
|
+
modelHome.set(name, moduleName);
|
|
260
|
+
}
|
|
261
|
+
for (const doc of ir.firestore) {
|
|
262
|
+
const moduleName = docModuleName(doc);
|
|
263
|
+
const deps = docDeps.get(doc.name) ?? { enums: [], models: [] };
|
|
264
|
+
const blocks = [...headerBlocks(context), "import { z } from 'zod'"];
|
|
265
|
+
// Sibling imports: _meta_ plus enums/embedded schemas hosted elsewhere.
|
|
266
|
+
const external = new Map();
|
|
267
|
+
for (const name of deps.enums) {
|
|
268
|
+
const home = enumHome.get(name);
|
|
269
|
+
if (home && home !== moduleName) {
|
|
270
|
+
const irEnum = findEnum(ir, name);
|
|
271
|
+
const set = external.get(home) ?? new Set();
|
|
272
|
+
set.add(`${irEnum ? fsEnumName(irEnum) : name}Schema`);
|
|
273
|
+
external.set(home, set);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const name of deps.models) {
|
|
277
|
+
const home = modelHome.get(name);
|
|
278
|
+
if (home && home !== moduleName) {
|
|
279
|
+
const set = external.get(home) ?? new Set();
|
|
280
|
+
set.add(`${fsModelName(ir, name)}Schema`);
|
|
281
|
+
external.set(home, set);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const importLines = [...external.entries()]
|
|
285
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
286
|
+
.map(([home, symbols]) => `import { ${[...symbols].sort().join(', ')} } from './${home}'`);
|
|
287
|
+
if (doc.meta)
|
|
288
|
+
importLines.unshift(`import { _Meta_Schema } from './_'`);
|
|
289
|
+
// Fields whose Zod chain is identical to the Data Connect schema are
|
|
290
|
+
// picked from it (single definition); only representation changes extend.
|
|
291
|
+
const baseModel = doc.from ? findModel(ir, doc.from) : undefined;
|
|
292
|
+
const { rendered } = effectiveFields(ir, doc);
|
|
293
|
+
const pickNames = baseModel ? rendered.filter(f => f.pickable).map(f => f.name) : [];
|
|
294
|
+
if (baseModel && pickNames.length > 0) {
|
|
295
|
+
importLines.unshift(`import { ${baseModel.name}Schema as Dc${baseModel.name}Schema } from '../schemas/${kebabCase(tableNameOf(baseModel))}'`);
|
|
296
|
+
}
|
|
297
|
+
if (importLines.length > 0)
|
|
298
|
+
blocks.push(importLines.join('\n'));
|
|
299
|
+
for (const name of deps.enums) {
|
|
300
|
+
if (enumHome.get(name) !== moduleName)
|
|
301
|
+
continue;
|
|
302
|
+
const irEnum = findEnum(ir, name);
|
|
303
|
+
if (irEnum)
|
|
304
|
+
blocks.push(renderFsEnum(irEnum));
|
|
305
|
+
}
|
|
306
|
+
for (const name of deps.models) {
|
|
307
|
+
if (modelHome.get(name) !== moduleName)
|
|
308
|
+
continue;
|
|
309
|
+
const model = findModel(ir, name);
|
|
310
|
+
if (model)
|
|
311
|
+
blocks.push(renderEmbeddedModel(ir, model));
|
|
312
|
+
}
|
|
313
|
+
if (doc.helpers)
|
|
314
|
+
blocks.push(doc.helpers);
|
|
315
|
+
const lines = [];
|
|
316
|
+
if (doc.collection)
|
|
317
|
+
lines.push(`// ${doc.collection}`);
|
|
318
|
+
lines.push(...lineComments(doc.description, ''));
|
|
319
|
+
if (baseModel && pickNames.length > 0) {
|
|
320
|
+
// DC スキーマと同一表現のフィールドは pick で流用し、FS で表現が変わる
|
|
321
|
+
// もの(timestamp→Date, optional→nullable, encode 済み id 等)だけ extend する。
|
|
322
|
+
lines.push(`export const ${doc.name}Schema = Dc${baseModel.name}Schema.pick({`);
|
|
323
|
+
for (const name of pickNames)
|
|
324
|
+
lines.push(` ${name}: true,`);
|
|
325
|
+
lines.push(`}).extend({`);
|
|
326
|
+
for (const field of rendered) {
|
|
327
|
+
if (field.pickable)
|
|
328
|
+
continue;
|
|
329
|
+
lines.push(...field.comments);
|
|
330
|
+
lines.push(field.entry);
|
|
331
|
+
}
|
|
332
|
+
if (doc.meta)
|
|
333
|
+
lines.push(' _meta_: _Meta_Schema,');
|
|
334
|
+
lines.push('})');
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
lines.push(`export const ${doc.name}Schema = z.object({`);
|
|
338
|
+
for (const field of rendered) {
|
|
339
|
+
lines.push(...field.comments);
|
|
340
|
+
lines.push(field.entry);
|
|
341
|
+
}
|
|
342
|
+
if (doc.meta)
|
|
343
|
+
lines.push(' _meta_: _Meta_Schema,');
|
|
344
|
+
lines.push('})');
|
|
345
|
+
}
|
|
346
|
+
lines.push(`export type ${doc.name} = z.infer<typeof ${doc.name}Schema>`);
|
|
347
|
+
blocks.push(lines.join('\n'));
|
|
348
|
+
files.push({ path: `firestore/${moduleName}.ts`, content: `${blocks.join('\n\n')}\n` });
|
|
349
|
+
}
|
|
350
|
+
if (ir.firestore.some(doc => doc.meta)) {
|
|
351
|
+
files.push({ path: 'firestore/_/_meta_.ts', content: META_FILE });
|
|
352
|
+
files.push({ path: 'firestore/_/index.ts', content: `export * from './_meta_'\n` });
|
|
353
|
+
}
|
|
354
|
+
// Barrel: database name constants (when the project defines services) + re-exports.
|
|
355
|
+
const barrel = [...headerBlocks(context)];
|
|
356
|
+
const services = ir.project?.services ?? [];
|
|
357
|
+
if (services.length > 0) {
|
|
358
|
+
const entries = services.map(s => ` ${constantCase(s.name)}: ${singleQuote(s.database ?? s.name)},`);
|
|
359
|
+
barrel.push([
|
|
360
|
+
'/** Firestore database 名定数 (firebase.json と同期させること) */',
|
|
361
|
+
'export const FIRESTORE_DATABASES = Object.freeze({',
|
|
362
|
+
...entries,
|
|
363
|
+
'} as const)',
|
|
364
|
+
'export type FirestoreDatabaseKey = keyof typeof FIRESTORE_DATABASES',
|
|
365
|
+
'export type FirestoreDatabaseId = (typeof FIRESTORE_DATABASES)[FirestoreDatabaseKey]',
|
|
366
|
+
].join('\n'));
|
|
367
|
+
}
|
|
368
|
+
const exportLines = ir.firestore.map(doc => `export * from './firestore/${docModuleName(doc)}'`);
|
|
369
|
+
if (ir.firestore.some(doc => doc.meta))
|
|
370
|
+
exportLines.push(`export * from './firestore/_'`);
|
|
371
|
+
barrel.push(['// Zod schema / TypeScript 型のエクスポート', ...exportLines].join('\n'));
|
|
372
|
+
files.push({ path: 'firestore.ts', content: `${barrel.join('\n\n')}\n` });
|
|
373
|
+
return files;
|
|
374
|
+
},
|
|
375
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Generator } from '../generator.js';
|
|
2
|
+
export interface FirestoreGeneratorOptions {
|
|
3
|
+
/** Module to import `Timestamp` from. Defaults to `firebase/firestore`. */
|
|
4
|
+
timestampImport?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Generates Firestore-oriented TypeScript document types: `timestamp`/`date`
|
|
8
|
+
* map to the Firestore `Timestamp` class and each model gets a `WithId` helper
|
|
9
|
+
* for reading documents that carry their id.
|
|
10
|
+
*/
|
|
11
|
+
export declare const createFirestoreTypeGenerator: (options?: FirestoreGeneratorOptions) => Generator;
|
|
12
|
+
//# sourceMappingURL=firestore-type-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firestore-type-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/firestore/firestore-type-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAI5E,MAAM,WAAW,yBAAyB;IACxC,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AA0DD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAS,yBAA8B,KAAG,SAiCtF,CAAA"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { headerBlocks } from '../support/header.js';
|
|
2
|
+
import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
|
|
3
|
+
const scalarMap = () => ({
|
|
4
|
+
string: 'string',
|
|
5
|
+
int: 'number',
|
|
6
|
+
int64: 'number',
|
|
7
|
+
float: 'number',
|
|
8
|
+
boolean: 'boolean',
|
|
9
|
+
timestamp: 'Timestamp',
|
|
10
|
+
date: 'Timestamp',
|
|
11
|
+
json: 'Json',
|
|
12
|
+
id: 'string',
|
|
13
|
+
});
|
|
14
|
+
const JSON_TYPE = [
|
|
15
|
+
'export type Json =',
|
|
16
|
+
' | string',
|
|
17
|
+
' | number',
|
|
18
|
+
' | boolean',
|
|
19
|
+
' | null',
|
|
20
|
+
' | Json[]',
|
|
21
|
+
' | { [key: string]: Json }',
|
|
22
|
+
].join('\n');
|
|
23
|
+
const renderRef = (ref, scalars) => {
|
|
24
|
+
switch (ref.kind) {
|
|
25
|
+
case 'scalar':
|
|
26
|
+
return scalars[ref.name];
|
|
27
|
+
case 'enum':
|
|
28
|
+
case 'model':
|
|
29
|
+
return ref.name;
|
|
30
|
+
default:
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const renderField = (field, scalars, ir) => {
|
|
35
|
+
const optional = field.optional ? '?' : '';
|
|
36
|
+
const doc = field.description ? ` /** ${field.description} */\n` : '';
|
|
37
|
+
// Firestore projections store relations as resolved foreign-key ids.
|
|
38
|
+
if (isRelation(field)) {
|
|
39
|
+
const base = scalars[relationFkType(ir, field)];
|
|
40
|
+
const type = field.list ? `${base}[]` : base;
|
|
41
|
+
return `${doc} ${relationFkName(field)}${optional}: ${type}`;
|
|
42
|
+
}
|
|
43
|
+
const base = renderRef(field.type, scalars);
|
|
44
|
+
const type = field.list ? `${base}[]` : base;
|
|
45
|
+
return `${doc} ${field.name}${optional}: ${type}`;
|
|
46
|
+
};
|
|
47
|
+
const usesTimestamp = (ir) => ir.models.some(model => model.fields.some(field => field.type.kind === 'scalar' && (field.type.name === 'timestamp' || field.type.name === 'date')));
|
|
48
|
+
const usesJson = (ir) => ir.models.some(model => model.fields.some(field => field.type.kind === 'scalar' && field.type.name === 'json'));
|
|
49
|
+
/**
|
|
50
|
+
* Generates Firestore-oriented TypeScript document types: `timestamp`/`date`
|
|
51
|
+
* map to the Firestore `Timestamp` class and each model gets a `WithId` helper
|
|
52
|
+
* for reading documents that carry their id.
|
|
53
|
+
*/
|
|
54
|
+
export const createFirestoreTypeGenerator = (options = {}) => {
|
|
55
|
+
const timestampImport = options.timestampImport ?? 'firebase/firestore';
|
|
56
|
+
const scalars = scalarMap();
|
|
57
|
+
return {
|
|
58
|
+
name: 'firestore-types',
|
|
59
|
+
description: 'Firestore document TypeScript types',
|
|
60
|
+
generate(ir, context) {
|
|
61
|
+
const blocks = [...headerBlocks(context)];
|
|
62
|
+
if (usesTimestamp(ir)) {
|
|
63
|
+
blocks.push(`import { Timestamp } from '${timestampImport}'`);
|
|
64
|
+
}
|
|
65
|
+
if (usesJson(ir)) {
|
|
66
|
+
blocks.push(JSON_TYPE);
|
|
67
|
+
}
|
|
68
|
+
blocks.push('export type WithId<T> = T & { id: string }');
|
|
69
|
+
for (const irEnum of ir.enums) {
|
|
70
|
+
const union = irEnum.values.map(value => `'${value}'`).join(' | ') || 'never';
|
|
71
|
+
blocks.push(`export type ${irEnum.name} = ${union}`);
|
|
72
|
+
}
|
|
73
|
+
for (const model of ir.models) {
|
|
74
|
+
const doc = model.description ? `/** ${model.description} */\n` : '';
|
|
75
|
+
const fields = model.fields.map(field => renderField(field, scalars, ir)).join('\n');
|
|
76
|
+
blocks.push(`${doc}export interface ${model.name} {\n${fields}\n}`);
|
|
77
|
+
}
|
|
78
|
+
return [{ path: 'firestore-types.ts', content: `${blocks.join('\n\n')}\n` }];
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Ir } from '../ir/ir.js';
|
|
2
|
+
export interface GeneratedFile {
|
|
3
|
+
/** Path relative to the generation output directory. */
|
|
4
|
+
path: string;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
export interface GeneratorContext {
|
|
8
|
+
/** Optional banner prepended to each generated file. */
|
|
9
|
+
header?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A code generator turns the IR into one or more output files. Implementations
|
|
13
|
+
* MUST depend only on the IR — never on YAML, raw documents, other generators,
|
|
14
|
+
* or shared mutable state. That independence is what lets generators be added,
|
|
15
|
+
* removed, or run in isolation.
|
|
16
|
+
*/
|
|
17
|
+
export interface Generator {
|
|
18
|
+
/** Unique key used to select the generator from the CLI/registry. */
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly description?: string;
|
|
21
|
+
generate(ir: Ir, context?: GeneratorContext): GeneratedFile[];
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/generators/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAEhC,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,aAAa,EAAE,CAAA;CAC9D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Generator } from '../generator.js';
|
|
2
|
+
export interface IdCodecGeneratorOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Module the generic `encodeNumericId`/`decodeNumericId`/`encodeStringId`/
|
|
5
|
+
* `decodeStringId` primitives are imported from. Defaults to `./id-core.js`.
|
|
6
|
+
*/
|
|
7
|
+
core?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Generates typed per-entity id encode/decode wrappers (`encodeShopId`,
|
|
11
|
+
* `decodeProductId`, …) over the generic Sqids/hashids primitives — instead of
|
|
12
|
+
* scattering untyped `encodeNumericId` calls at every call site, the wrappers
|
|
13
|
+
* make the int↔string boundary explicit and entity-safe.
|
|
14
|
+
*/
|
|
15
|
+
export declare const createIdCodecGenerator: (options?: IdCodecGeneratorOptions) => Generator;
|
|
16
|
+
//# sourceMappingURL=id-codec-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id-codec-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/id/id-codec-generator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAG5E,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AA2GD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,UAAS,uBAA4B,KAAG,SA8B9E,CAAA"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { pascalCase } from '../support/naming.js';
|
|
2
|
+
import { headerBlocks } from '../support/header.js';
|
|
3
|
+
const idFieldOf = (model) => {
|
|
4
|
+
const ids = model.fields.filter(field => field.isId);
|
|
5
|
+
return ids.length === 1 ? ids[0] : undefined;
|
|
6
|
+
};
|
|
7
|
+
/** Numeric (Int64/bigserial) ids use the numeric codec; everything else the string codec. */
|
|
8
|
+
const isNumericId = (field) => field.type.kind === 'scalar' && (field.type.name === 'int' || field.type.name === 'int64');
|
|
9
|
+
/**
|
|
10
|
+
* Self-contained Sqids primitives, emitted when the contract configures
|
|
11
|
+
* `project.idCodec`, so the Sqids alphabet/minLength stay a contract-level
|
|
12
|
+
* constant shared by every service.
|
|
13
|
+
*/
|
|
14
|
+
const idCoreFile = (idCodec) => `import Sqids from 'sqids'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ID 短縮用の Sqids インスタンス
|
|
18
|
+
*
|
|
19
|
+
* minLength: 最小${idCodec.minLength ?? 8}文字(短すぎないように)
|
|
20
|
+
* alphabet: URL-safe な文字のみ(シャッフル済み)
|
|
21
|
+
*/
|
|
22
|
+
const sqids = new Sqids({
|
|
23
|
+
minLength: ${idCodec.minLength ?? 8},
|
|
24
|
+
alphabet: '${idCodec.alphabet ?? ''}',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 数値 ID(DC の BIGINT 等)を Sqids でエンコードして短い URL-safe 文字列にする。
|
|
29
|
+
*
|
|
30
|
+
* @param id - 数値 ID(string 形式の数値、JS の safe integer 範囲内)
|
|
31
|
+
* @returns 短縮文字列
|
|
32
|
+
* @throws safe integer 範囲を超える場合
|
|
33
|
+
*/
|
|
34
|
+
export const encodeNumericId = (id: string): string => {
|
|
35
|
+
if (id === '') {
|
|
36
|
+
throw new Error('Cannot encode empty ID')
|
|
37
|
+
}
|
|
38
|
+
const numId = Number(id)
|
|
39
|
+
if (!Number.isSafeInteger(numId)) {
|
|
40
|
+
throw new Error(\`ID exceeds safe integer range: \${id}\`)
|
|
41
|
+
}
|
|
42
|
+
return sqids.encode([numId])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* \`encodeNumericId\` でエンコードされた文字列を元の数値文字列に戻す。
|
|
47
|
+
*
|
|
48
|
+
* @param encoded - encodeNumericId の出力
|
|
49
|
+
* @returns DC の ID(string 形式)
|
|
50
|
+
* @throws デコードできない場合
|
|
51
|
+
*/
|
|
52
|
+
export const decodeNumericId = (encoded: string): string => {
|
|
53
|
+
const decoded = sqids.decode(encoded)
|
|
54
|
+
const value = decoded[0]
|
|
55
|
+
if (value === undefined) {
|
|
56
|
+
throw new Error(\`Invalid encoded numeric ID: \${encoded}\`)
|
|
57
|
+
}
|
|
58
|
+
return value.toString()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 任意の文字列を Sqids でエンコードする。
|
|
63
|
+
*
|
|
64
|
+
* 文字列の各コードポイント (UTF-16 サロゲートペアも 1 文字として扱う) を
|
|
65
|
+
* 数値配列に変換し、 sqids にかける。 結果は URL-safe な短縮文字列。
|
|
66
|
+
*
|
|
67
|
+
* 用途: Firebase uid のような英数字 ID を、 数値 ID と同じ枠組みで扱いたいとき。
|
|
68
|
+
* 文字列長に比例して結果も長くなるので、 短縮効果は薄い (難読化が主目的)。
|
|
69
|
+
*/
|
|
70
|
+
export const encodeStringId = (str: string): string => {
|
|
71
|
+
if (str === '') {
|
|
72
|
+
throw new Error('Cannot encode empty string')
|
|
73
|
+
}
|
|
74
|
+
const codePoints = Array.from(str).map(c => c.codePointAt(0) ?? 0)
|
|
75
|
+
return sqids.encode(codePoints)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* \`encodeStringId\` でエンコードされた文字列を元の文字列に戻す。
|
|
80
|
+
*
|
|
81
|
+
* @throws デコードできない場合
|
|
82
|
+
*/
|
|
83
|
+
export const decodeStringId = (encoded: string): string => {
|
|
84
|
+
const codePoints = sqids.decode(encoded)
|
|
85
|
+
if (codePoints.length === 0) {
|
|
86
|
+
throw new Error(\`Invalid encoded string ID: \${encoded}\`)
|
|
87
|
+
}
|
|
88
|
+
return String.fromCodePoint(...codePoints)
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
const renderCodec = (model, field) => {
|
|
92
|
+
const name = pascalCase(model.name);
|
|
93
|
+
if (isNumericId(field)) {
|
|
94
|
+
return [
|
|
95
|
+
`export const encode${name}Id = (id: number | string): string => encodeNumericId(String(id))`,
|
|
96
|
+
`export const decode${name}Id = (encoded: string): string => decodeNumericId(encoded)`,
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
return [
|
|
100
|
+
`export const encode${name}Id = (id: string): string => encodeStringId(id)`,
|
|
101
|
+
`export const decode${name}Id = (encoded: string): string => decodeStringId(encoded)`,
|
|
102
|
+
].join('\n');
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Generates typed per-entity id encode/decode wrappers (`encodeShopId`,
|
|
106
|
+
* `decodeProductId`, …) over the generic Sqids/hashids primitives — instead of
|
|
107
|
+
* scattering untyped `encodeNumericId` calls at every call site, the wrappers
|
|
108
|
+
* make the int↔string boundary explicit and entity-safe.
|
|
109
|
+
*/
|
|
110
|
+
export const createIdCodecGenerator = (options = {}) => {
|
|
111
|
+
const core = options.core ?? './id-core.js';
|
|
112
|
+
return {
|
|
113
|
+
name: 'id-codecs',
|
|
114
|
+
description: 'Typed per-entity id encode/decode wrappers',
|
|
115
|
+
generate(ir, context) {
|
|
116
|
+
const entries = ir.models
|
|
117
|
+
.map(model => ({ model, field: idFieldOf(model) }))
|
|
118
|
+
.filter((entry) => !!entry.field);
|
|
119
|
+
if (entries.length === 0) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const needsNumeric = entries.some(e => isNumericId(e.field));
|
|
123
|
+
const needsString = entries.some(e => !isNumericId(e.field));
|
|
124
|
+
const imports = [
|
|
125
|
+
...(needsNumeric ? ['encodeNumericId', 'decodeNumericId'] : []),
|
|
126
|
+
...(needsString ? ['encodeStringId', 'decodeStringId'] : []),
|
|
127
|
+
];
|
|
128
|
+
const blocks = [...headerBlocks(context), `import { ${imports.join(', ')} } from '${core}'`];
|
|
129
|
+
for (const { model, field } of entries) {
|
|
130
|
+
blocks.push(renderCodec(model, field));
|
|
131
|
+
}
|
|
132
|
+
const files = [{ path: 'id-codecs.ts', content: `${blocks.join('\n\n')}\n` }];
|
|
133
|
+
// Emit the primitives too when the contract pins the Sqids settings.
|
|
134
|
+
if (ir.project?.idCodec && core === './id-core.js') {
|
|
135
|
+
files.push({ path: 'id-core.ts', content: idCoreFile(ir.project.idCodec) });
|
|
136
|
+
}
|
|
137
|
+
return files;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
};
|