@pyric/ui 0.1.0-alpha.11 → 0.1.0-alpha.13
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/package.json +5 -4
- package/src/agents/ContextWindowUsage.tsx +514 -0
- package/src/agents/EmptyState.tsx +27 -0
- package/src/agents/Fold.tsx +64 -0
- package/src/agents/Modal.tsx +65 -0
- package/src/agents/PulsingDot.tsx +28 -0
- package/src/agents/inbrowser-agent-usage.d.ts +141 -0
- package/src/agents/index.ts +28 -0
- package/src/auth/authApi.ts +72 -0
- package/src/auth/claims.ts +63 -0
- package/src/auth/components/AuthProviderToggles.tsx +147 -0
- package/src/auth/components/AuthSignInHelper.tsx +197 -0
- package/src/auth/components/AuthUserForm.tsx +328 -0
- package/src/auth/components/AuthUserList.tsx +219 -0
- package/src/auth/components/ClaimsField.tsx +50 -0
- package/src/auth/components/confirmActions.tsx +114 -0
- package/src/auth/controller.ts +173 -0
- package/src/auth/hooks/index.ts +36 -0
- package/src/auth/hooks/useAuthFlowHelper.ts +55 -0
- package/src/auth/hooks/useAuthProviderConfig.ts +139 -0
- package/src/auth/hooks/useAuthUserEditor.ts +76 -0
- package/src/auth/hooks/useAuthUsers.ts +154 -0
- package/src/auth/index.ts +42 -0
- package/src/auth/providers.ts +28 -0
- package/src/auth/reducers/userEditor.ts +186 -0
- package/src/events/components/ActivityActionItems.tsx +136 -0
- package/src/events/components/ActivityGrid.tsx +197 -0
- package/src/events/components/ActivityGridRow.tsx +65 -0
- package/src/events/components/ProposedChangeDiff.tsx +175 -0
- package/src/events/components/format.ts +21 -0
- package/src/events/components/index.ts +17 -0
- package/src/events/digest.ts +630 -0
- package/src/events/hooks/index.ts +9 -0
- package/src/events/hooks/useActivityDigest.ts +42 -0
- package/src/events/hooks/useActivityStream.ts +86 -0
- package/src/events/index.ts +39 -0
- package/src/events/types.ts +152 -0
- package/src/firestore/components/CollectionList.tsx +78 -0
- package/src/firestore/components/DeleteWithConfirm.tsx +88 -0
- package/src/firestore/components/DocumentEditor.tsx +350 -0
- package/src/firestore/components/DocumentList.tsx +217 -0
- package/src/firestore/components/DocumentPreview.tsx +265 -0
- package/src/firestore/components/FieldRenderer.tsx +25 -0
- package/src/firestore/components/QueryBuilder.tsx +181 -0
- package/src/firestore/components/ReferencePicker.tsx +212 -0
- package/src/firestore/components/TreeEntry.tsx +58 -0
- package/src/firestore/components/context.ts +25 -0
- package/src/firestore/fieldEditors/array.tsx +30 -0
- package/src/firestore/fieldEditors/boolean.tsx +39 -0
- package/src/firestore/fieldEditors/bytes.tsx +53 -0
- package/src/firestore/fieldEditors/geopoint.tsx +71 -0
- package/src/firestore/fieldEditors/map.tsx +33 -0
- package/src/firestore/fieldEditors/null.tsx +27 -0
- package/src/firestore/fieldEditors/number.tsx +43 -0
- package/src/firestore/fieldEditors/reference.tsx +87 -0
- package/src/firestore/fieldEditors/registry.ts +45 -0
- package/src/firestore/fieldEditors/string.tsx +33 -0
- package/src/firestore/fieldEditors/timestamp.tsx +75 -0
- package/src/firestore/fieldEditors/types.ts +68 -0
- package/src/firestore/fieldEditors/vector.tsx +142 -0
- package/src/firestore/firestoreApi.ts +86 -0
- package/src/firestore/hooks/coerceError.ts +34 -0
- package/src/firestore/hooks/index.ts +46 -0
- package/src/firestore/hooks/useCollectionList.ts +102 -0
- package/src/firestore/hooks/useDocumentEditor.ts +161 -0
- package/src/firestore/hooks/useDocumentList.ts +242 -0
- package/src/firestore/hooks/useDocumentSubcollections.ts +86 -0
- package/src/firestore/hooks/useFirestoreCollection.ts +51 -0
- package/src/firestore/hooks/useFirestoreDoc.ts +55 -0
- package/src/firestore/hooks/useQueryBuilder.ts +188 -0
- package/src/firestore/hooks/useRecursiveDelete.ts +77 -0
- package/src/firestore/hooks/useReferencePicker.ts +228 -0
- package/src/firestore/import/parseImport.ts +137 -0
- package/src/firestore/index.ts +87 -0
- package/src/firestore/reducers/defaults.ts +38 -0
- package/src/firestore/reducers/documentEditor.ts +239 -0
- package/src/firestore/reducers/tree.ts +140 -0
- package/src/firestore/reducers/types.ts +92 -0
- package/src/firestore/reducers/validation.ts +129 -0
- package/src/firestore/types.ts +231 -0
- package/src/firestore/validation/ids.ts +45 -0
- package/src/firestore/valueEquality.ts +43 -0
- package/src/index.ts +10 -0
- package/src/primitives/Badge.tsx +40 -0
- package/src/primitives/ConfirmDialog.tsx +137 -0
- package/src/primitives/CopyButton.tsx +62 -0
- package/src/primitives/JsonView.tsx +151 -0
- package/src/primitives/SegmentedControl.tsx +72 -0
- package/src/primitives/Toast.tsx +161 -0
- package/src/primitives/VirtualList.tsx +104 -0
- package/src/primitives/hooks/useContainerSize.ts +53 -0
- package/src/primitives/hooks/useUpdateHighlights.ts +109 -0
- package/src/primitives/index.ts +39 -0
- package/src/primitives/useConfirm.tsx +113 -0
- package/src/rtdb/components/RtdbPathBar.tsx +135 -0
- package/src/rtdb/components/RtdbTree.tsx +409 -0
- package/src/rtdb/editor.ts +79 -0
- package/src/rtdb/hooks/useRtdbTree.ts +137 -0
- package/src/rtdb/index.ts +66 -0
- package/src/rtdb/pathInput.ts +47 -0
- package/src/rtdb/reducers/tree.ts +191 -0
- package/src/rtdb/rtdbApi.ts +23 -0
- package/src/rtdb/values.ts +188 -0
- package/src/rules/components/DenialInspector.tsx +227 -0
- package/src/rules/components/format.ts +171 -0
- package/src/rules/components/index.ts +12 -0
- package/src/rules/components/scope.ts +75 -0
- package/src/rules/hooks/index.ts +5 -0
- package/src/rules/hooks/useDenialTrace.ts +100 -0
- package/src/rules/index.ts +24 -0
- package/src/rules/types.ts +91 -0
- package/src/storage/collisionRename.ts +114 -0
- package/src/storage/components/DeleteSelectionWithConfirm.tsx +193 -0
- package/src/storage/components/ObjectBrowser.tsx +219 -0
- package/src/storage/components/ObjectInspector.tsx +169 -0
- package/src/storage/components/PathBreadcrumb.tsx +84 -0
- package/src/storage/components/UploadDropzone.tsx +182 -0
- package/src/storage/folderPlaceholder.ts +40 -0
- package/src/storage/hooks/index.ts +59 -0
- package/src/storage/hooks/useMetadataEditor.ts +329 -0
- package/src/storage/hooks/useObjectUpload.ts +262 -0
- package/src/storage/hooks/usePathState.ts +94 -0
- package/src/storage/hooks/useStorageDelete.ts +195 -0
- package/src/storage/hooks/useStorageList.ts +261 -0
- package/src/storage/hooks/useStorageObject.ts +162 -0
- package/src/storage/hooks/useStorageRulesGate.ts +270 -0
- package/src/storage/hooks/useStorageSelection.ts +90 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/pendingPrefixes.ts +125 -0
- package/src/storage/previews.tsx +120 -0
- package/src/storage/storageApi.ts +54 -0
- package/src/traffic/components/RuleHeatmap.tsx +97 -0
- package/src/traffic/components/TrafficDetail.tsx +160 -0
- package/src/traffic/components/TrafficGroupRow.tsx +91 -0
- package/src/traffic/components/TrafficLineChart.tsx +139 -0
- package/src/traffic/components/TrafficLog.tsx +175 -0
- package/src/traffic/components/TrafficMetricCards.tsx +77 -0
- package/src/traffic/components/TrafficRow.tsx +69 -0
- package/src/traffic/components/TrafficStats.tsx +73 -0
- package/src/traffic/components/TrafficTimeline.tsx +289 -0
- package/src/traffic/components/format.ts +22 -0
- package/src/traffic/components/index.ts +22 -0
- package/src/traffic/hooks/index.ts +60 -0
- package/src/traffic/hooks/useRuleHeatmap.ts +92 -0
- package/src/traffic/hooks/useTrafficBuckets.ts +146 -0
- package/src/traffic/hooks/useTrafficFilter.ts +74 -0
- package/src/traffic/hooks/useTrafficGroups.ts +126 -0
- package/src/traffic/hooks/useTrafficMetrics.ts +250 -0
- package/src/traffic/hooks/useTrafficMonitor.ts +111 -0
- package/src/traffic/hooks/useTrafficStats.ts +77 -0
- package/src/traffic/index.ts +13 -0
- package/src/traffic/types.ts +85 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
doc as docFn,
|
|
4
|
+
getDocs,
|
|
5
|
+
limit as limitFn,
|
|
6
|
+
query as queryFn,
|
|
7
|
+
type CollectionReference,
|
|
8
|
+
type DocumentReference,
|
|
9
|
+
type Firestore,
|
|
10
|
+
type QueryDocumentSnapshot,
|
|
11
|
+
} from 'pyric/firestore';
|
|
12
|
+
|
|
13
|
+
export type BrowseLocation =
|
|
14
|
+
| { kind: 'root' }
|
|
15
|
+
| { kind: 'document'; ref: DocumentReference }
|
|
16
|
+
| { kind: 'collection'; ref: CollectionReference };
|
|
17
|
+
|
|
18
|
+
export interface UseReferencePickerOptions {
|
|
19
|
+
firestore: Firestore;
|
|
20
|
+
/**
|
|
21
|
+
* Lister for subcollections under a parent (or root when
|
|
22
|
+
* `parent == null`). The library does not ship a default — see
|
|
23
|
+
* `useCollectionList` for the same rationale (the modular Web SDK
|
|
24
|
+
* can't enumerate collections client-side).
|
|
25
|
+
*/
|
|
26
|
+
listCollections: (
|
|
27
|
+
firestore: Firestore,
|
|
28
|
+
parent: DocumentReference | null,
|
|
29
|
+
) => Promise<CollectionReference[]>;
|
|
30
|
+
/** Default page size for the document list when browsing inside
|
|
31
|
+
* a collection. Default 20. */
|
|
32
|
+
pageSize?: number;
|
|
33
|
+
/** Initial value to pre-populate the text input + parse. */
|
|
34
|
+
initialPath?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface UseReferencePickerResult {
|
|
38
|
+
/** Current text input value. */
|
|
39
|
+
pathInput: string;
|
|
40
|
+
/** Validated `DocumentReference` parsed from `pathInput`, or
|
|
41
|
+
* `null` when the path is empty / invalid. */
|
|
42
|
+
reference: DocumentReference | null;
|
|
43
|
+
/** Parse error, or `null` when valid. */
|
|
44
|
+
error: string | null;
|
|
45
|
+
/** Current browse position in the tree. */
|
|
46
|
+
browseLocation: BrowseLocation;
|
|
47
|
+
/** Whether `drillBack` has anywhere to go. */
|
|
48
|
+
canDrillBack: boolean;
|
|
49
|
+
/** Collections available at the current browse level. Populated
|
|
50
|
+
* when `browseLocation` is `root` or `document`. */
|
|
51
|
+
collections: CollectionReference[];
|
|
52
|
+
/** First page of documents in the current collection — populated
|
|
53
|
+
* when `browseLocation.kind === 'collection'`. */
|
|
54
|
+
documents: QueryDocumentSnapshot[];
|
|
55
|
+
/** True while a fetch is in flight. */
|
|
56
|
+
isLoading: boolean;
|
|
57
|
+
|
|
58
|
+
/** Set the text-input value. Parses on every change. */
|
|
59
|
+
setPathInput: (path: string) => void;
|
|
60
|
+
/** Commit a chosen reference. Updates `pathInput` (and therefore
|
|
61
|
+
* the parsed `reference`). */
|
|
62
|
+
pick: (ref: DocumentReference) => void;
|
|
63
|
+
/** Drill into a collection — fetches its first page of documents. */
|
|
64
|
+
drillIntoCollection: (ref: CollectionReference) => void;
|
|
65
|
+
/** Drill into a document — fetches its subcollections. */
|
|
66
|
+
drillIntoDocument: (ref: DocumentReference) => void;
|
|
67
|
+
/** Step back one level. No-op when at root. */
|
|
68
|
+
drillBack: () => void;
|
|
69
|
+
/** Clear the path input + reset browse to root. */
|
|
70
|
+
clear: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface BrowseState {
|
|
74
|
+
current: BrowseLocation;
|
|
75
|
+
history: BrowseLocation[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseReferencePath(
|
|
79
|
+
firestore: Firestore,
|
|
80
|
+
path: string,
|
|
81
|
+
): { ref: DocumentReference | null; error: string | null } {
|
|
82
|
+
const trimmed = path.trim();
|
|
83
|
+
if (!trimmed) return { ref: null, error: null };
|
|
84
|
+
const segments = trimmed.split('/').filter(Boolean);
|
|
85
|
+
if (segments.length === 0) return { ref: null, error: 'Empty path' };
|
|
86
|
+
if (segments.length % 2 !== 0)
|
|
87
|
+
return { ref: null, error: 'Must point to a document (even segment count)' };
|
|
88
|
+
try {
|
|
89
|
+
const ref = docFn(firestore, trimmed) as DocumentReference;
|
|
90
|
+
return { ref, error: null };
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return {
|
|
93
|
+
ref: null,
|
|
94
|
+
error: e instanceof Error ? e.message : 'Invalid path',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ROOT: BrowseLocation = { kind: 'root' };
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Picker state machine. Browses a Firestore tree level-by-level
|
|
103
|
+
* (root → collection → document → collection → ...), maintains a
|
|
104
|
+
* separately-validated text-input path, and commits a chosen
|
|
105
|
+
* reference via `pick`.
|
|
106
|
+
*
|
|
107
|
+
* Headless — consumers compose the resulting state into their own
|
|
108
|
+
* UI, or use the bundled `<ReferencePicker>` component.
|
|
109
|
+
*/
|
|
110
|
+
export function useReferencePicker({
|
|
111
|
+
firestore,
|
|
112
|
+
listCollections,
|
|
113
|
+
pageSize = 20,
|
|
114
|
+
initialPath = '',
|
|
115
|
+
}: UseReferencePickerOptions): UseReferencePickerResult {
|
|
116
|
+
const [pathInput, setPathInputState] = useState(initialPath);
|
|
117
|
+
const [browse, setBrowse] = useState<BrowseState>({
|
|
118
|
+
current: ROOT,
|
|
119
|
+
history: [],
|
|
120
|
+
});
|
|
121
|
+
const [collections, setCollections] = useState<CollectionReference[]>([]);
|
|
122
|
+
const [documents, setDocuments] = useState<QueryDocumentSnapshot[]>([]);
|
|
123
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
124
|
+
|
|
125
|
+
const parsed = useMemo(
|
|
126
|
+
() => parseReferencePath(firestore, pathInput),
|
|
127
|
+
[firestore, pathInput],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Keep the injected `listCollections` in a ref so the fetch
|
|
131
|
+
// effect's deps don't include it. Consumers commonly pass an
|
|
132
|
+
// inline arrow function whose identity churns on every render;
|
|
133
|
+
// depending on it would loop the effect forever.
|
|
134
|
+
const listCollectionsRef = useRef(listCollections);
|
|
135
|
+
listCollectionsRef.current = listCollections;
|
|
136
|
+
|
|
137
|
+
// Re-fetch contents whenever the browse location changes.
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
let cancelled = false;
|
|
140
|
+
setIsLoading(true);
|
|
141
|
+
const loc = browse.current;
|
|
142
|
+
if (loc.kind === 'root' || loc.kind === 'document') {
|
|
143
|
+
const parent = loc.kind === 'document' ? loc.ref : null;
|
|
144
|
+
setDocuments([]);
|
|
145
|
+
listCollectionsRef.current(firestore, parent)
|
|
146
|
+
.then((cs) => {
|
|
147
|
+
if (cancelled) return;
|
|
148
|
+
setCollections(cs);
|
|
149
|
+
setIsLoading(false);
|
|
150
|
+
})
|
|
151
|
+
.catch(() => {
|
|
152
|
+
if (cancelled) return;
|
|
153
|
+
setCollections([]);
|
|
154
|
+
setIsLoading(false);
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
setCollections([]);
|
|
158
|
+
const pagedQuery = queryFn(loc.ref, limitFn(pageSize));
|
|
159
|
+
getDocs(pagedQuery)
|
|
160
|
+
.then((snap) => {
|
|
161
|
+
if (cancelled) return;
|
|
162
|
+
setDocuments([...snap.docs]);
|
|
163
|
+
setIsLoading(false);
|
|
164
|
+
})
|
|
165
|
+
.catch(() => {
|
|
166
|
+
if (cancelled) return;
|
|
167
|
+
setDocuments([]);
|
|
168
|
+
setIsLoading(false);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return () => {
|
|
172
|
+
cancelled = true;
|
|
173
|
+
};
|
|
174
|
+
}, [browse.current, firestore, pageSize]);
|
|
175
|
+
|
|
176
|
+
const setPathInput = useCallback((path: string) => {
|
|
177
|
+
setPathInputState(path);
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
const pick = useCallback((ref: DocumentReference) => {
|
|
181
|
+
setPathInputState(ref.path);
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
const drillIntoCollection = useCallback((ref: CollectionReference) => {
|
|
185
|
+
setBrowse((prev) => ({
|
|
186
|
+
current: { kind: 'collection', ref },
|
|
187
|
+
history: [...prev.history, prev.current],
|
|
188
|
+
}));
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const drillIntoDocument = useCallback((ref: DocumentReference) => {
|
|
192
|
+
setBrowse((prev) => ({
|
|
193
|
+
current: { kind: 'document', ref },
|
|
194
|
+
history: [...prev.history, prev.current],
|
|
195
|
+
}));
|
|
196
|
+
}, []);
|
|
197
|
+
|
|
198
|
+
const drillBack = useCallback(() => {
|
|
199
|
+
setBrowse((prev) => {
|
|
200
|
+
if (prev.history.length === 0) return prev;
|
|
201
|
+
const next = prev.history.slice(0, -1);
|
|
202
|
+
const popped = prev.history[prev.history.length - 1];
|
|
203
|
+
return { current: popped, history: next };
|
|
204
|
+
});
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
207
|
+
const clear = useCallback(() => {
|
|
208
|
+
setPathInputState('');
|
|
209
|
+
setBrowse({ current: ROOT, history: [] });
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
pathInput,
|
|
214
|
+
reference: parsed.ref,
|
|
215
|
+
error: parsed.error,
|
|
216
|
+
browseLocation: browse.current,
|
|
217
|
+
canDrillBack: browse.history.length > 0,
|
|
218
|
+
collections,
|
|
219
|
+
documents,
|
|
220
|
+
isLoading,
|
|
221
|
+
setPathInput,
|
|
222
|
+
pick,
|
|
223
|
+
drillIntoCollection,
|
|
224
|
+
drillIntoDocument,
|
|
225
|
+
drillBack,
|
|
226
|
+
clear,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON import parsing + collision detection for the Firestore "import JSON
|
|
3
|
+
* into a collection" flow.
|
|
4
|
+
*
|
|
5
|
+
* Pure, no React, no I/O: `parseImport` takes the raw pasted/loaded text and
|
|
6
|
+
* returns the documents it would create plus any per-item errors; the caller
|
|
7
|
+
* (the Studio pane) owns showing the "will create N documents" preview,
|
|
8
|
+
* disclosing a collision policy ONLY when {@link detectCollisions} finds an
|
|
9
|
+
* overlap, and actually writing through the existing Firestore write handle.
|
|
10
|
+
*
|
|
11
|
+
* Two accepted shapes:
|
|
12
|
+
* 1. A map: `{ "docId": { ...fields }, "docId2": { ...fields } }`
|
|
13
|
+
* — each key becomes the document id (validated), each value its data.
|
|
14
|
+
* 2. An array: `[ { ...fields }, { ...fields } ]`
|
|
15
|
+
* — each element becomes a document with `id: null` (auto-id at write
|
|
16
|
+
* time), since a bare array carries no natural id.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { validateDocumentId } from '../validation/ids.js';
|
|
20
|
+
|
|
21
|
+
/** One document to create. `id === null` means "let Firestore auto-id it"
|
|
22
|
+
* (only produced by the array shape when no `generateId` option is given —
|
|
23
|
+
* a map key is always a chosen id). */
|
|
24
|
+
export interface ParsedImportDoc {
|
|
25
|
+
id: string | null;
|
|
26
|
+
data: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ParseImportOptions {
|
|
30
|
+
/** When provided, array-shape entries get their auto-id GENERATED AT PARSE
|
|
31
|
+
* TIME (instead of `id: null` / addDoc-at-write-time). Fixing ids at parse
|
|
32
|
+
* makes a retry after a partial failure idempotent: the same parse's ids
|
|
33
|
+
* are reused, so re-running the import cannot duplicate already-written
|
|
34
|
+
* docs. Use {@link firestoreAutoId} for prod-parity ids. */
|
|
35
|
+
generateId?: () => string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const AUTO_ID_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
39
|
+
|
|
40
|
+
/** A Firestore-style 20-char auto id (same alphabet/length the SDK uses). */
|
|
41
|
+
export function firestoreAutoId(): string {
|
|
42
|
+
let id = '';
|
|
43
|
+
for (let i = 0; i < 20; i++) {
|
|
44
|
+
id += AUTO_ID_ALPHABET.charAt(Math.floor(Math.random() * AUTO_ID_ALPHABET.length));
|
|
45
|
+
}
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ParseImportResult {
|
|
50
|
+
docs: ParsedImportDoc[];
|
|
51
|
+
/** Human-readable problems found while parsing. A non-empty `errors` does
|
|
52
|
+
* NOT necessarily mean `docs` is empty — the parser is per-item tolerant
|
|
53
|
+
* so one bad entry doesn't block the rest; the caller decides whether to
|
|
54
|
+
* block on any error or proceed with the valid subset. */
|
|
55
|
+
errors: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
59
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse raw JSON text into the documents it would create. Never throws —
|
|
64
|
+
* a JSON syntax error or a wrong top-level shape becomes an entry in
|
|
65
|
+
* `errors` with an empty `docs` array.
|
|
66
|
+
*/
|
|
67
|
+
export function parseImport(input: string, options?: ParseImportOptions): ParseImportResult {
|
|
68
|
+
const trimmed = input.trim();
|
|
69
|
+
if (trimmed === '') {
|
|
70
|
+
return { docs: [], errors: ['Input is empty'] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let parsed: unknown;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(trimmed);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { docs: [], errors: [`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const docs: ParsedImportDoc[] = [];
|
|
81
|
+
const errors: string[] = [];
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(parsed)) {
|
|
84
|
+
parsed.forEach((item, i) => {
|
|
85
|
+
if (!isPlainObject(item)) {
|
|
86
|
+
errors.push(`Item ${i}: expected an object, got ${describeType(item)}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
docs.push({ id: options?.generateId ? options.generateId() : null, data: item });
|
|
90
|
+
});
|
|
91
|
+
return { docs, errors };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isPlainObject(parsed)) {
|
|
95
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
96
|
+
const idError = validateDocumentId(key);
|
|
97
|
+
if (idError) {
|
|
98
|
+
errors.push(`"${key}": invalid document id — ${idError}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!isPlainObject(value)) {
|
|
102
|
+
errors.push(`"${key}": expected an object of fields, got ${describeType(value)}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
docs.push({ id: key, data: value });
|
|
106
|
+
}
|
|
107
|
+
return { docs, errors };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
docs: [],
|
|
112
|
+
errors: ['Input must be a JSON object mapping docId -> fields, or an array of objects'],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function describeType(value: unknown): string {
|
|
117
|
+
if (value === null) return 'null';
|
|
118
|
+
if (Array.isArray(value)) return 'an array';
|
|
119
|
+
return typeof value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Ids in `docs` (map-shape entries only — `id !== null`) that already exist
|
|
124
|
+
* in `existingIds`. The UI shows the skip-or-overwrite policy choice ONLY
|
|
125
|
+
* when this returns a non-empty list.
|
|
126
|
+
*/
|
|
127
|
+
export function detectCollisions(
|
|
128
|
+
existingIds: readonly string[],
|
|
129
|
+
docs: readonly ParsedImportDoc[],
|
|
130
|
+
): string[] {
|
|
131
|
+
const existing = new Set(existingIds);
|
|
132
|
+
const collisions: string[] = [];
|
|
133
|
+
for (const doc of docs) {
|
|
134
|
+
if (doc.id !== null && existing.has(doc.id)) collisions.push(doc.id);
|
|
135
|
+
}
|
|
136
|
+
return collisions;
|
|
137
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export * from './hooks/index.js';
|
|
2
|
+
|
|
3
|
+
// Injectable Firestore API bundle (Pyric Studio data-backend swap): defaults to
|
|
4
|
+
// in-process `pyric/firestore`; a consumer can provide the SharedWorker client.
|
|
5
|
+
export {
|
|
6
|
+
FirestoreApiProvider,
|
|
7
|
+
useFirestoreApi,
|
|
8
|
+
type FirestoreApi,
|
|
9
|
+
} from './firestoreApi.js';
|
|
10
|
+
|
|
11
|
+
// M2: read-only display surface
|
|
12
|
+
export {
|
|
13
|
+
inferType,
|
|
14
|
+
asVectorView,
|
|
15
|
+
vectorPreview,
|
|
16
|
+
truncateVectorsForDisplay,
|
|
17
|
+
type FieldType,
|
|
18
|
+
type VectorView,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
export { firestoreValuesEqual } from './valueEquality.js';
|
|
21
|
+
export {
|
|
22
|
+
defaultFieldEditors,
|
|
23
|
+
mergeFieldEditors,
|
|
24
|
+
} from './fieldEditors/registry.js';
|
|
25
|
+
export type {
|
|
26
|
+
FieldEditorContract,
|
|
27
|
+
FieldEditorRegistry,
|
|
28
|
+
FieldDisplayProps,
|
|
29
|
+
FieldEditProps,
|
|
30
|
+
} from './fieldEditors/types.js';
|
|
31
|
+
export { FieldRenderer, type FieldRendererProps } from './components/FieldRenderer.js';
|
|
32
|
+
export {
|
|
33
|
+
DocumentPreview,
|
|
34
|
+
type DocumentPreviewProps,
|
|
35
|
+
} from './components/DocumentPreview.js';
|
|
36
|
+
|
|
37
|
+
// M3: editor surface
|
|
38
|
+
export type {
|
|
39
|
+
FieldNode,
|
|
40
|
+
EditorTree,
|
|
41
|
+
DocumentEditorAction,
|
|
42
|
+
DocumentEditorState,
|
|
43
|
+
} from './reducers/types.js';
|
|
44
|
+
export { validateLeaf, validateTree } from './reducers/validation.js';
|
|
45
|
+
export { treeFromData, treeToData } from './reducers/tree.js';
|
|
46
|
+
export { initState, reducer } from './reducers/documentEditor.js';
|
|
47
|
+
export {
|
|
48
|
+
DocumentEditor,
|
|
49
|
+
DocumentEditorRoot,
|
|
50
|
+
DocumentEditorFields,
|
|
51
|
+
useDocumentEditorContext,
|
|
52
|
+
type DocumentEditorRootProps,
|
|
53
|
+
} from './components/DocumentEditor.js';
|
|
54
|
+
|
|
55
|
+
// M4: operational read/write + admin ops
|
|
56
|
+
export { CollectionList, type CollectionListProps } from './components/CollectionList.js';
|
|
57
|
+
export { DocumentList, type DocumentListProps } from './components/DocumentList.js';
|
|
58
|
+
export {
|
|
59
|
+
DeleteWithConfirm,
|
|
60
|
+
type DeleteWithConfirmProps,
|
|
61
|
+
} from './components/DeleteWithConfirm.js';
|
|
62
|
+
|
|
63
|
+
// M5: improvements over firebase-tools-ui
|
|
64
|
+
export {
|
|
65
|
+
ReferencePicker,
|
|
66
|
+
type ReferencePickerProps,
|
|
67
|
+
} from './components/ReferencePicker.js';
|
|
68
|
+
|
|
69
|
+
// M6: query builder + virtualization
|
|
70
|
+
export {
|
|
71
|
+
QueryBuilder,
|
|
72
|
+
type QueryBuilderProps,
|
|
73
|
+
} from './components/QueryBuilder.js';
|
|
74
|
+
|
|
75
|
+
// M7: create-collection / create-document / JSON-import — pure, tested logic.
|
|
76
|
+
// Presentational wiring lives in the consumer (Pyric Studio's FirestorePane)
|
|
77
|
+
// over these + the existing DocumentEditor/hooks, per the disclosure-over-
|
|
78
|
+
// modals design principles.
|
|
79
|
+
export { validateCollectionId, validateDocumentId } from './validation/ids.js';
|
|
80
|
+
export {
|
|
81
|
+
parseImport,
|
|
82
|
+
detectCollisions,
|
|
83
|
+
firestoreAutoId,
|
|
84
|
+
type ParsedImportDoc,
|
|
85
|
+
type ParseImportResult,
|
|
86
|
+
type ParseImportOptions,
|
|
87
|
+
} from './import/parseImport.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Timestamp, GeoPoint, Bytes } from 'pyric/firestore';
|
|
2
|
+
import type { FieldType } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default value used when a new field of this type is added (or
|
|
6
|
+
* when an existing field's type is switched). The reducer applies
|
|
7
|
+
* these so a fresh node always starts in a valid state for its type
|
|
8
|
+
* — except that map/array start empty (children come from add
|
|
9
|
+
* actions).
|
|
10
|
+
*/
|
|
11
|
+
export function defaultValueFor(type: FieldType): unknown {
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'string':
|
|
14
|
+
return '';
|
|
15
|
+
case 'number':
|
|
16
|
+
return 0;
|
|
17
|
+
case 'boolean':
|
|
18
|
+
return false;
|
|
19
|
+
case 'null':
|
|
20
|
+
return null;
|
|
21
|
+
case 'timestamp':
|
|
22
|
+
return Timestamp.now();
|
|
23
|
+
case 'geopoint':
|
|
24
|
+
return new GeoPoint(0, 0);
|
|
25
|
+
case 'reference':
|
|
26
|
+
// No default — a fresh reference is invalid until the user
|
|
27
|
+
// sets a path. The validator will surface that.
|
|
28
|
+
return { path: '', id: '', firestore: {} };
|
|
29
|
+
case 'bytes':
|
|
30
|
+
return Bytes.fromUint8Array(new Uint8Array());
|
|
31
|
+
case 'vector':
|
|
32
|
+
// The wire-sentinel both inferType and the vector editor speak.
|
|
33
|
+
return { __type__: '__vector__', value: [] };
|
|
34
|
+
case 'map':
|
|
35
|
+
case 'array':
|
|
36
|
+
return undefined; // children carry the value
|
|
37
|
+
}
|
|
38
|
+
}
|