@promptbook/cli 0.103.0-45 → 0.103.0-47
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/apps/agents-server/config.ts.todo +4 -4
- package/apps/agents-server/next.config.ts +3 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +86 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/book/test.http +37 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +6 -4
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/TODO.txt +1 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/route.ts +53 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/modelRequirements/systemMessage/route.ts +45 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +54 -0
- package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +140 -0
- package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +25 -0
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/{SelfLearningBook.tsx → AgentBookAndChatComponent.tsx} +5 -46
- package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +7 -4
- package/apps/agents-server/src/app/agents/[agentName]/chat/AgentChatWrapper.tsx +38 -0
- package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +23 -0
- package/apps/agents-server/src/app/agents/[agentName]/page.tsx +13 -6
- package/apps/agents-server/src/app/agents/page.tsx +11 -0
- package/apps/agents-server/src/app/api/chat/route.ts +1 -1
- package/apps/agents-server/src/app/api/chat-streaming/route.ts +5 -1
- package/apps/agents-server/src/app/api/upload/route.ts +75 -0
- package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +1 -0
- package/apps/agents-server/src/tools/$provideCdnForServer.ts +28 -0
- package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +119 -0
- package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +32 -0
- package/apps/agents-server/src/utils/cdn/interfaces/IStorage.ts +14 -0
- package/apps/agents-server/src/utils/cdn/utils/getUserFileCdnKey.ts +27 -0
- package/apps/agents-server/src/utils/cdn/utils/nameToSubfolderPath.ts +9 -0
- package/apps/agents-server/src/utils/cdn/utils/nextRequestToNodeRequest.ts +27 -0
- package/apps/agents-server/src/utils/validators/validateMimeType.ts +24 -0
- package/apps/agents-server/tsconfig.json +1 -1
- package/esm/index.es.js +191 -165
- package/esm/index.es.js.map +1 -1
- package/esm/typings/servers.d.ts +1 -7
- package/esm/typings/src/_packages/components.index.d.ts +4 -0
- package/esm/typings/src/_packages/core.index.d.ts +16 -14
- package/esm/typings/src/_packages/types.index.d.ts +12 -6
- package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +6 -1
- package/esm/typings/src/book-2.0/agent-source/AgentSourceParseResult.d.ts +1 -1
- package/esm/typings/src/book-2.0/agent-source/createCommitmentRegex.d.ts +1 -1
- package/esm/typings/src/book-2.0/agent-source/padBook.d.ts +2 -0
- package/esm/typings/src/book-2.0/agent-source/string_book.d.ts +2 -0
- package/esm/typings/src/book-components/Chat/AgentChat/AgentChat.d.ts +14 -0
- package/esm/typings/src/book-components/Chat/AgentChat/AgentChat.test.d.ts +1 -0
- package/esm/typings/src/book-components/Chat/AgentChat/AgentChatProps.d.ts +13 -0
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +5 -60
- package/esm/typings/src/{book-2.0/commitments → commitments}/ACTION/ACTION.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/DELETE/DELETE.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/FORMAT/FORMAT.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/GOAL/GOAL.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/KNOWLEDGE/KNOWLEDGE.d.ts +1 -5
- package/esm/typings/src/{book-2.0/commitments → commitments}/MEMORY/MEMORY.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/MESSAGE/MESSAGE.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/META/META.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/META_IMAGE/META_IMAGE.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/META_LINK/META_LINK.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/MODEL/MODEL.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/NOTE/NOTE.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/PERSONA/PERSONA.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/RULE/RULE.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/SAMPLE/SAMPLE.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/SCENARIO/SCENARIO.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/STYLE/STYLE.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/_base/BaseCommitmentDefinition.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/_base/CommitmentDefinition.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/_base/NotYetImplementedCommitmentDefinition.d.ts +1 -1
- package/esm/typings/src/{book-2.0/commitments → commitments}/_base/createEmptyAgentModelRequirements.d.ts +1 -1
- package/esm/typings/src/conversion/validation/validatePipeline.d.ts +2 -0
- package/esm/typings/src/execution/LlmExecutionTools.d.ts +1 -1
- package/esm/typings/src/execution/utils/validatePromptResult.d.ts +2 -0
- package/esm/typings/src/llm-providers/agent/Agent.d.ts +3 -7
- package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +1 -1
- package/esm/typings/src/llm-providers/agent/CreateAgentLlmExecutionToolsOptions.d.ts +1 -1
- package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +32 -0
- package/esm/typings/src/llm-providers/agent/RemoteAgentOptions.d.ts +11 -0
- package/esm/typings/src/llm-providers/openai/OpenAiAssistantExecutionTools.d.ts +5 -1
- package/esm/typings/src/pipeline/validatePipelineString.d.ts +2 -0
- package/esm/typings/src/storage/_common/PromptbookStorage.d.ts +1 -0
- package/esm/typings/src/types/typeAliases.d.ts +6 -0
- package/esm/typings/src/utils/color/internal-utils/checkChannelValue.d.ts +0 -3
- package/esm/typings/src/utils/random/$generateBookBoilerplate.d.ts +2 -2
- package/esm/typings/src/utils/random/$randomFullnameWithColor.d.ts +1 -1
- package/esm/typings/src/utils/validators/parameterName/validateParameterName.d.ts +2 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +191 -165
- package/umd/index.umd.js.map +1 -1
- /package/esm/typings/src/{book-2.0/commitments → commitments}/_base/BookCommitment.d.ts +0 -0
- /package/esm/typings/src/{book-2.0/commitments → commitments}/_base/ParsedCommitment.d.ts +0 -0
- /package/esm/typings/src/{book-2.0/commitments → commitments}/index.d.ts +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { headers } from 'next/headers';
|
|
4
|
+
import { $sideEffect } from '../../../../../../../src/utils/organization/$sideEffect';
|
|
5
|
+
import { AgentChatWrapper } from './AgentChatWrapper';
|
|
6
|
+
|
|
7
|
+
export default async function AgentChatPage({ params }: { params: Promise<{ agentName: string }> }) {
|
|
8
|
+
$sideEffect(headers());
|
|
9
|
+
let { agentName } = await params;
|
|
10
|
+
agentName = decodeURIComponent(agentName);
|
|
11
|
+
|
|
12
|
+
const agentUrl = `/agents/${agentName}`;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<main className={`w-screen h-screen`}>
|
|
16
|
+
<AgentChatWrapper agentUrl={agentUrl} />
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* TODO: [🚗] Components and pages here should be just tiny UI wraper around proper agent logic and conponents
|
|
23
|
+
*/
|
|
@@ -18,14 +18,16 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
|
|
|
18
18
|
|
|
19
19
|
$sideEffect(headers());
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
let { agentName } = await params;
|
|
22
|
+
agentName = decodeURIComponent(agentName);
|
|
23
|
+
|
|
22
24
|
const collection = await $provideAgentCollectionForServer();
|
|
23
|
-
const
|
|
24
|
-
const agentSource = agentSourceSubject.getValue();
|
|
25
|
+
const agentSource = await collection.getAgentSource(agentName);
|
|
25
26
|
const agentProfile = parseAgentSource(agentSource);
|
|
26
27
|
|
|
27
28
|
// Build agent page URL for QR and copy
|
|
28
|
-
const pageUrl =
|
|
29
|
+
const pageUrl = `${process.env.NEXT_PUBLIC_URL}/agents/${encodeURIComponent(agentName)}`;
|
|
30
|
+
// <- TODO: !!! Better
|
|
29
31
|
|
|
30
32
|
// Extract brand color from meta
|
|
31
33
|
const brandColor = agentProfile.meta.color || '#3b82f6'; // Default to blue-600
|
|
@@ -124,13 +126,15 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
|
|
|
124
126
|
</div>
|
|
125
127
|
<div className="flex gap-4 mt-6">
|
|
126
128
|
<a
|
|
127
|
-
href=
|
|
129
|
+
href={`${pageUrl}/chat`}
|
|
130
|
+
// <- !!!! Can I append path like this on current browser URL in href?
|
|
128
131
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded shadow font-semibold transition"
|
|
129
132
|
>
|
|
130
133
|
💬 Chat
|
|
131
134
|
</a>
|
|
132
135
|
<a
|
|
133
|
-
href=
|
|
136
|
+
href={`${pageUrl}/book`}
|
|
137
|
+
// <- !!!! Can I append path like this on current browser URL in href?
|
|
134
138
|
className="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded shadow font-semibold transition"
|
|
135
139
|
>
|
|
136
140
|
✏️ Edit Agent Book
|
|
@@ -151,5 +155,8 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
|
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
/**
|
|
158
|
+
* TODO: !!! Make this page look nice - 🃏
|
|
159
|
+
* TODO: !!! Show usage of LLM
|
|
154
160
|
* TODO: [🚗] Components and pages here should be just tiny UI wraper around proper agent logic and conponents
|
|
161
|
+
* TODO: [🎣][🧠] Maybe do API / Page for transpilers, Allow to export each agent
|
|
155
162
|
*/
|
|
@@ -39,6 +39,10 @@ export async function GET(request: Request) {
|
|
|
39
39
|
|
|
40
40
|
return new Response(readableStream, {
|
|
41
41
|
status: 200,
|
|
42
|
-
headers: { 'Content-Type': 'text/
|
|
42
|
+
headers: { 'Content-Type': 'text/markdown' },
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Note: [🐚] This is how streaming is implemented correctly
|
|
48
|
+
*/
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { nextRequestToNodeRequest } from '@/src/utils/cdn/utils/nextRequestToNodeRequest';
|
|
2
|
+
import { TODO_any } from '@promptbook-local/types';
|
|
3
|
+
import { serializeError } from '@promptbook-local/utils';
|
|
4
|
+
import formidable from 'formidable';
|
|
5
|
+
import { readFile } from 'fs/promises';
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
7
|
+
import { assertsError } from '../../../../../../src/errors/assertsError';
|
|
8
|
+
import { string_url } from '../../../../../../src/types/typeAliases';
|
|
9
|
+
import { keepUnused } from '../../../../../../src/utils/organization/keepUnused';
|
|
10
|
+
import { $provideCdnForServer } from '../../../../src/tools/$provideCdnForServer';
|
|
11
|
+
import { getUserFileCdnKey } from '../../../../src/utils/cdn/utils/getUserFileCdnKey';
|
|
12
|
+
import { validateMimeType } from '../../../../src/utils/validators/validateMimeType';
|
|
13
|
+
|
|
14
|
+
export async function POST(request: NextRequest) {
|
|
15
|
+
try {
|
|
16
|
+
const nodeRequest = await nextRequestToNodeRequest(request);
|
|
17
|
+
|
|
18
|
+
const files = await new Promise<formidable.Files>((resolve, reject) => {
|
|
19
|
+
const form = formidable({});
|
|
20
|
+
form.parse(nodeRequest as TODO_any, (error, fields, files) => {
|
|
21
|
+
keepUnused(fields);
|
|
22
|
+
|
|
23
|
+
if (error) {
|
|
24
|
+
return reject(error);
|
|
25
|
+
}
|
|
26
|
+
resolve(files);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const uploadedFiles = files.file;
|
|
31
|
+
|
|
32
|
+
if (!uploadedFiles || uploadedFiles.length !== 1) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ message: 'In form data there is not EXACTLY one "file" field' },
|
|
35
|
+
{ status: 400 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const uploadedFile = uploadedFiles[0]!;
|
|
40
|
+
const fileBuffer = await readFile(uploadedFile.filepath);
|
|
41
|
+
const cdn = $provideCdnForServer();
|
|
42
|
+
const key = getUserFileCdnKey(fileBuffer, uploadedFile.originalFilename || uploadedFile.newFilename);
|
|
43
|
+
|
|
44
|
+
await cdn.setItem(key, {
|
|
45
|
+
type: validateMimeType(uploadedFile.mimetype),
|
|
46
|
+
data: fileBuffer,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const fileUrl = cdn.getItemUrl(key);
|
|
50
|
+
|
|
51
|
+
return NextResponse.json({ fileUrl: fileUrl.href as string_url }, { status: 201 });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
assertsError(error);
|
|
54
|
+
|
|
55
|
+
console.error(error);
|
|
56
|
+
|
|
57
|
+
return new Response(
|
|
58
|
+
JSON.stringify(
|
|
59
|
+
serializeError(error),
|
|
60
|
+
// <- TODO: !!! Rename `serializeError` to `errorToJson`
|
|
61
|
+
null,
|
|
62
|
+
4,
|
|
63
|
+
// <- TODO: !!! Allow to configure pretty print for agent server
|
|
64
|
+
),
|
|
65
|
+
{
|
|
66
|
+
status: 400, // <- TODO: !!! Make `errorToHttpStatusCode`
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* TODO: !!!! Is this Working on Vercel
|
|
75
|
+
*/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DigitalOceanSpaces } from '../utils/cdn/classes/DigitalOceanSpaces';
|
|
2
|
+
import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cache of CDN instance
|
|
6
|
+
*
|
|
7
|
+
* @private internal cache for `$provideCdnForServer`
|
|
8
|
+
*/
|
|
9
|
+
let cdn: IIFilesStorageWithCdn | null = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* !!!
|
|
13
|
+
*/
|
|
14
|
+
export function $provideCdnForServer(): IIFilesStorageWithCdn {
|
|
15
|
+
if (!cdn) {
|
|
16
|
+
cdn = new DigitalOceanSpaces({
|
|
17
|
+
bucket: process.env.CDN_BUCKET!,
|
|
18
|
+
pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
|
|
19
|
+
endpoint: process.env.CDN_ENDPOINT!,
|
|
20
|
+
accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
|
|
21
|
+
secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
|
|
22
|
+
cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
|
|
23
|
+
gzip: true,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return cdn;
|
|
28
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { GetObjectCommand, PutObjectCommand, PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { NotYetImplementedError } from '@promptbook-local/core';
|
|
3
|
+
import { gzip, ungzip } from 'node-gzip';
|
|
4
|
+
import { TODO_USE } from '../../../../../../src/utils/organization/TODO_USE';
|
|
5
|
+
import { validateMimeType } from '../../validators/validateMimeType';
|
|
6
|
+
import type { IFile, IIFilesStorageWithCdn } from '../interfaces/IFilesStorage';
|
|
7
|
+
|
|
8
|
+
type IDigitalOceanSpacesConfig = {
|
|
9
|
+
readonly bucket: string;
|
|
10
|
+
readonly pathPrefix: string;
|
|
11
|
+
readonly endpoint: string;
|
|
12
|
+
readonly accessKeyId: string;
|
|
13
|
+
readonly secretAccessKey: string;
|
|
14
|
+
readonly cdnPublicUrl: URL;
|
|
15
|
+
readonly gzip: boolean;
|
|
16
|
+
|
|
17
|
+
// TODO: [⛳️] Probbably prefix should be in this config not on the consumer side
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
|
|
21
|
+
public get cdnPublicUrl() {
|
|
22
|
+
return this.config.cdnPublicUrl;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private s3: S3Client;
|
|
26
|
+
|
|
27
|
+
public constructor(private readonly config: IDigitalOceanSpacesConfig) {
|
|
28
|
+
this.s3 = new S3Client({
|
|
29
|
+
region: 'auto',
|
|
30
|
+
endpoint: 'https://' + config.endpoint,
|
|
31
|
+
credentials: {
|
|
32
|
+
accessKeyId: config.accessKeyId,
|
|
33
|
+
secretAccessKey: config.secretAccessKey,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getItemUrl(key: string): URL {
|
|
39
|
+
return new URL(this.config.pathPrefix + '/' + key, this.cdnPublicUrl);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async getItem(key: string): Promise<IFile | null> {
|
|
43
|
+
const parameters = {
|
|
44
|
+
Bucket: this.config.bucket,
|
|
45
|
+
Key: this.config.pathPrefix + '/' + key,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const { Body, ContentType, ContentEncoding } = await this.s3.send(new GetObjectCommand(parameters));
|
|
50
|
+
|
|
51
|
+
// const blob = new Blob([await Body?.transformToByteArray()!]);
|
|
52
|
+
|
|
53
|
+
if (ContentEncoding === 'gzip') {
|
|
54
|
+
return {
|
|
55
|
+
type: validateMimeType(ContentType),
|
|
56
|
+
data: await ungzip(await Body!.transformToByteArray()),
|
|
57
|
+
};
|
|
58
|
+
} else {
|
|
59
|
+
return {
|
|
60
|
+
type: validateMimeType(ContentType),
|
|
61
|
+
data: (await Body!.transformToByteArray()) as Buffer,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof Error && error.name.match(/^NoSuchKey/)) {
|
|
66
|
+
return null;
|
|
67
|
+
} else {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async removeItem(key: string): Promise<void> {
|
|
74
|
+
TODO_USE(key);
|
|
75
|
+
throw new NotYetImplementedError(`DigitalOceanSpaces.removeItem is not implemented yet`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public async setItem(key: string, file: IFile): Promise<void> {
|
|
79
|
+
// TODO: Put putObjectRequestAdditional into processedFile
|
|
80
|
+
const putObjectRequestAdditional: Partial<PutObjectCommandInput> = {};
|
|
81
|
+
|
|
82
|
+
let processedFile: IFile;
|
|
83
|
+
if (this.config.gzip) {
|
|
84
|
+
const gzipped = await gzip(file.data);
|
|
85
|
+
const sizePercentageAfterCompression = gzipped.byteLength / file.data.byteLength;
|
|
86
|
+
if (sizePercentageAfterCompression < 0.7) {
|
|
87
|
+
// consolex.log(`Gzipping ${key} (${Math.floor(sizePercentageAfterCompression * 100)}%)`);
|
|
88
|
+
processedFile = { ...file, data: gzipped };
|
|
89
|
+
putObjectRequestAdditional.ContentEncoding = 'gzip';
|
|
90
|
+
} else {
|
|
91
|
+
processedFile = file;
|
|
92
|
+
// consolex.log(`NOT Gzipping ${key} (${Math.floor(sizePercentageAfterCompression * 100)}%)`);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
processedFile = file;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const uploadResult = await this.s3.send(
|
|
99
|
+
new PutObjectCommand({
|
|
100
|
+
Bucket: this.config.bucket,
|
|
101
|
+
Key: this.config.pathPrefix + '/' + key,
|
|
102
|
+
ContentType: processedFile.type,
|
|
103
|
+
...putObjectRequestAdditional,
|
|
104
|
+
Body: processedFile.data,
|
|
105
|
+
// TODO: Public read access / just private to extending class
|
|
106
|
+
ACL: 'public-read',
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!uploadResult.ETag) {
|
|
111
|
+
throw new Error(`Upload result does not contain ETag`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* TODO: Implement Read-only mode
|
|
118
|
+
* TODO: [☹️] Unite with `PromptbookStorage` and move to `/src/...`
|
|
119
|
+
*/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { string_mime_type } from '../../../../../../src/types/typeAliases';
|
|
2
|
+
import type { IStorage } from './IStorage';
|
|
3
|
+
|
|
4
|
+
export type IFile = {
|
|
5
|
+
// Maybe TODO name: string_name;
|
|
6
|
+
type: string_mime_type;
|
|
7
|
+
data: Buffer;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents storage that will store each keypair in a separate file.
|
|
12
|
+
*/
|
|
13
|
+
export type IFilesStorage = Omit<IStorage<IFile>, 'length' | 'clear' | 'key'>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Represents storage that can give public deterministic URL for each file
|
|
17
|
+
*/
|
|
18
|
+
export type IIFilesStorageWithCdn = IFilesStorage & {
|
|
19
|
+
readonly cdnPublicUrl: URL;
|
|
20
|
+
getItemUrl(key: string): URL;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* TODO: Probably not deterministic and async getItemUrl
|
|
25
|
+
* TODO: Probably just createUrlMaker
|
|
26
|
+
* TODO: List method
|
|
27
|
+
* TODO: Glob method
|
|
28
|
+
* TODO: Subfolder (similar to PrefixStorage) method
|
|
29
|
+
* TODO: Subscribe, list, sub(folder) should be part of LIB everstorage
|
|
30
|
+
* TODO: Probably implement observe through RxJS
|
|
31
|
+
* TODO: [☹️] Unite with `PromptbookStorage` and move to `/src/...`
|
|
32
|
+
*/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Note: This is a simplified version of the IStorage interface based on the usage in the project.
|
|
2
|
+
export type IStorage<T> = {
|
|
3
|
+
readonly length: Promise<number>;
|
|
4
|
+
clear(): Promise<void>;
|
|
5
|
+
getItem(key: string): Promise<T | null>;
|
|
6
|
+
key(index: number): Promise<string | null>;
|
|
7
|
+
removeItem(key: string): Promise<void>;
|
|
8
|
+
setItem(key: string, value: T): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* TODO: [☹️] Unite with `PromptbookStorage` and move to `/src/...`
|
|
14
|
+
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { titleToName } from '../../../../../../src/utils/normalization/titleToName';
|
|
2
|
+
import hexEncoder from 'crypto-js/enc-hex';
|
|
3
|
+
import sha256 from 'crypto-js/sha256';
|
|
4
|
+
import type { string_uri } from '../../../../../../src/types/typeAliases';
|
|
5
|
+
import { nameToSubfolderPath } from './nameToSubfolderPath';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates a path for the user content
|
|
9
|
+
*/
|
|
10
|
+
export function getUserFileCdnKey(file: Buffer, originalFilename: string): string_uri {
|
|
11
|
+
const hash = sha256(hexEncoder.parse(file.toString('hex'))).toString(/* hex */);
|
|
12
|
+
|
|
13
|
+
const originalFilenameParts = originalFilename.split('.');
|
|
14
|
+
const extension = originalFilenameParts.pop();
|
|
15
|
+
const name = titleToName(originalFilenameParts.join('.'));
|
|
16
|
+
|
|
17
|
+
const filename = name + '.' + extension;
|
|
18
|
+
// <- Note: [⛳️] Preserving original file name
|
|
19
|
+
|
|
20
|
+
return `user/files/${nameToSubfolderPath(hash).join('/')}/${filename}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* TODO: [🌍] Unite this logic in one place
|
|
25
|
+
* TODO: Way to garbage unused uploaded files
|
|
26
|
+
* TODO: Probably separate util countBufferHash
|
|
27
|
+
*/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { string_name } from '../../../../../../src/types/typeAliases';
|
|
2
|
+
|
|
3
|
+
export function nameToSubfolderPath(name: string_name): Array<string> {
|
|
4
|
+
return [name.substr(0, 2).toLowerCase(), name.substr(2, 5).toLowerCase()];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TODO: !!! Use `nameToSubfolderPath` from src
|
|
9
|
+
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TODO_any } from '@promptbook-local/types';
|
|
2
|
+
import { NextRequest } from 'next/server';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
|
|
5
|
+
export async function nextRequestToNodeRequest(nextRequest: NextRequest): Promise<Readable> {
|
|
6
|
+
const reader = nextRequest.body?.getReader();
|
|
7
|
+
|
|
8
|
+
if (!reader) {
|
|
9
|
+
throw new Error(`Can not get nextRequest.body.getReader()`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const nodeStream = new Readable({
|
|
13
|
+
async read() {
|
|
14
|
+
const { done, value } = await reader.read();
|
|
15
|
+
if (done) this.push(null);
|
|
16
|
+
else this.push(Buffer.from(value));
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Fake IncomingMessage with headers
|
|
21
|
+
(nodeStream as TODO_any).headers = Object.fromEntries(nextRequest.headers.entries());
|
|
22
|
+
(nodeStream as TODO_any).method = nextRequest.method;
|
|
23
|
+
(nodeStream as TODO_any).url = nextRequest.url;
|
|
24
|
+
(nodeStream as TODO_any).socket = {}; // required by formidable
|
|
25
|
+
|
|
26
|
+
return nodeStream;
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { string_mime_type } from '../../../../../src/types/typeAliases';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the value is valid mime-type
|
|
5
|
+
*
|
|
6
|
+
* @param value candidate for mime-type
|
|
7
|
+
* @returns the value if it is valid mime-type
|
|
8
|
+
* @throws TypeError if the value is not valid mime-type
|
|
9
|
+
*/
|
|
10
|
+
export function validateMimeType(value: unknown): string_mime_type {
|
|
11
|
+
if (typeof value !== 'string') {
|
|
12
|
+
throw new TypeError(`Mime-type must be string, but it is ${typeof value}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!/^[a-z]+\/(?:[a-z0-9]+[.-])*[a-z0-9]+$/i.test(value)) {
|
|
16
|
+
throw new TypeError(`Invalid mime-type "${value}"`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return value as string_mime_type;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* TODO: [🧠] Move to main Promptbook utils
|
|
24
|
+
*/
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
"@promptbook-local/*": ["../../src/_packages/*.index"]
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
|
-
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
27
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../_common/hooks/usePromise.ts"],
|
|
28
28
|
"exclude": ["node_modules"]
|
|
29
29
|
}
|