Skip to content

Commit 125fa46

Browse files
committed
ci(modeling-commons): add more comprehensive seed data
1 parent 32d6e7c commit 125fa46

14 files changed

Lines changed: 1683 additions & 1515 deletions

File tree

apps/modeling-commons-backend/prisma/seed.ts

Lines changed: 3 additions & 775 deletions
Large diffs are not rendered by default.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { PutObjectCommand } from '@aws-sdk/client-s3';
4+
import { s3, bucket } from './providers.js';
5+
6+
const seedFilesPath = path.join(import.meta.dirname, '..', 'seed-files');
7+
8+
const MIME_BY_EXT: Record<string, string> = {
9+
'.png': 'image/png',
10+
'.jpg': 'image/jpeg',
11+
'.jpeg': 'image/jpeg',
12+
'.gif': 'image/gif',
13+
'.bmp': 'image/bmp',
14+
'.webp': 'image/webp',
15+
'.nlogox': 'application/xml',
16+
'.nlogo': 'text/plain',
17+
'.csv': 'text/csv',
18+
'.md': 'text/markdown',
19+
'.txt': 'text/plain',
20+
};
21+
22+
const PUBLIC_PREFIX = 'files/public';
23+
24+
function mimeFor(filename: string): string {
25+
return MIME_BY_EXT[path.extname(filename).toLowerCase()] ?? 'application/octet-stream';
26+
}
27+
28+
/** Pull the human-readable Info tab out of an nlogox file, if present. */
29+
function extractInfoTab(xml: string): string | undefined {
30+
const match = xml.match(/<info>\s*<!\[CDATA\[([\s\S]*?)\]\]>\s*<\/info>/);
31+
return match?.[1]?.trim() || undefined;
32+
}
33+
34+
export interface NlogoxAsset {
35+
/** S3 key for the (private) model file. */
36+
key: string;
37+
filename: string;
38+
blob: Buffer;
39+
contentType: string;
40+
sizeBytes: bigint;
41+
infoTab: string | undefined;
42+
preview?: PreviewAsset;
43+
}
44+
45+
export interface PreviewAsset {
46+
/** Public S3 key for the preview image. */
47+
key: string;
48+
filename: string;
49+
blob: Buffer;
50+
contentType: string;
51+
}
52+
53+
function loadPreview(previewFilename: string): PreviewAsset {
54+
const blob = fs.readFileSync(path.join(seedFilesPath, previewFilename));
55+
return {
56+
key: `${PUBLIC_PREFIX}/preview-images/seed/${previewFilename}`,
57+
filename: previewFilename,
58+
blob,
59+
contentType: mimeFor(previewFilename),
60+
};
61+
}
62+
63+
/** Load a real `.nlogox` file shipped in `seed-files/`, with its Info tab + optional preview. */
64+
export function loadNlogox(filename: string, previewFilename?: string): NlogoxAsset {
65+
const filepath = path.join(seedFilesPath, filename);
66+
const content = fs.readFileSync(filepath, 'utf-8');
67+
return {
68+
key: `uploads/models/${filename}`,
69+
filename,
70+
blob: Buffer.from(content, 'utf-8'),
71+
contentType: 'application/xml',
72+
sizeBytes: BigInt(fs.statSync(filepath).size),
73+
infoTab: extractInfoTab(content),
74+
preview: previewFilename ? loadPreview(previewFilename) : undefined,
75+
};
76+
}
77+
78+
/**
79+
* Synthesize a placeholder `.nlogox` for a library model we don't ship a real
80+
* file for. Cards still render (title, description, preview); the file just
81+
* won't open in the NetLogo viewer.
82+
*/
83+
export function fakeNlogox(slug: string, title: string, previewFilename?: string): NlogoxAsset {
84+
const xml = `<?xml version="1.0" encoding="utf-8" ?>
85+
<model version="NetLogo 7.0.0" snapToGrid="true">
86+
<code><![CDATA[;; ${title} - placeholder seed model
87+
to setup
88+
clear-all
89+
reset-ticks
90+
end
91+
92+
to go
93+
tick
94+
end]]></code>
95+
<info><![CDATA[## WHAT IS IT?
96+
97+
${title} is part of the NetLogo Models Library. This is a seeded placeholder used for local development.]]></info>
98+
</model>`;
99+
const filename = `${slug}.nlogox`;
100+
const blob = Buffer.from(xml, 'utf-8');
101+
return {
102+
key: `uploads/models/${filename}`,
103+
filename,
104+
blob,
105+
contentType: 'application/xml',
106+
sizeBytes: BigInt(blob.byteLength),
107+
infoTab: extractInfoTab(xml),
108+
preview: previewFilename ? loadPreview(previewFilename) : undefined,
109+
};
110+
}
111+
112+
export interface SupplementaryAsset {
113+
key: string;
114+
filename: string;
115+
blob: Buffer;
116+
contentType: string;
117+
sizeBytes: bigint;
118+
}
119+
120+
export function textAsset(
121+
filename: string,
122+
content: string,
123+
keyPrefix = 'uploads/models',
124+
): SupplementaryAsset {
125+
const blob = Buffer.from(content, 'utf-8');
126+
return {
127+
key: `${keyPrefix.replace(/\/+$/, '')}/${filename}`,
128+
filename,
129+
blob,
130+
contentType: mimeFor(filename),
131+
sizeBytes: BigInt(blob.byteLength),
132+
};
133+
}
134+
135+
/**
136+
* Uploads to object storage, de-duplicated by key. Public assets (preview
137+
* images) live under `files/public/` and are uploaded with a public-read ACL
138+
* so `fileService.getUrl` serves them directly.
139+
*/
140+
export class AssetUploader {
141+
private uploaded = new Set<string>();
142+
143+
async put(opts: {
144+
key: string;
145+
body: Buffer;
146+
contentType: string;
147+
filename: string;
148+
public?: boolean;
149+
}): Promise<void> {
150+
if (this.uploaded.has(opts.key)) return;
151+
await s3.send(
152+
new PutObjectCommand({
153+
Bucket: bucket,
154+
Key: opts.key,
155+
Body: opts.body,
156+
ContentType: opts.contentType,
157+
...(opts.public ? { ACL: 'public-read' } : {}),
158+
Metadata: { filename: opts.filename, createdAt: new Date().toISOString() },
159+
}),
160+
);
161+
this.uploaded.add(opts.key);
162+
}
163+
164+
async putNlogox(asset: NlogoxAsset): Promise<void> {
165+
await this.put({
166+
key: asset.key,
167+
body: asset.blob,
168+
contentType: asset.contentType,
169+
filename: asset.filename,
170+
});
171+
if (asset.preview) {
172+
await this.put({
173+
key: asset.preview.key,
174+
body: asset.preview.blob,
175+
contentType: asset.preview.contentType,
176+
filename: asset.preview.filename,
177+
public: true,
178+
});
179+
}
180+
}
181+
182+
async putSupplementary(asset: SupplementaryAsset): Promise<void> {
183+
await this.put({
184+
key: asset.key,
185+
body: asset.blob,
186+
contentType: asset.contentType,
187+
filename: asset.filename,
188+
});
189+
}
190+
191+
get count(): number {
192+
return this.uploaded.size;
193+
}
194+
}

0 commit comments

Comments
 (0)