unbrowse 1.1.4 → 1.1.5

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/dist/cli.js CHANGED
@@ -615,24 +615,28 @@ function buildEntityIndex(items) {
615
615
  function detectEntityIndex(data) {
616
616
  if (data == null || typeof data !== "object")
617
617
  return null;
618
- const obj = data;
619
- const candidates = [];
620
- if (Array.isArray(obj.included))
621
- candidates.push(obj.included);
622
- if (obj.data && typeof obj.data === "object") {
623
- const d = obj.data;
624
- if (Array.isArray(d.included))
625
- candidates.push(d.included);
626
- }
627
- for (const arr of candidates) {
618
+ let best = null;
619
+ const check = (arr) => {
628
620
  if (arr.length < 2)
629
- continue;
630
- const sample = arr.slice(0, 5);
621
+ return;
622
+ const sample = arr.slice(0, 10);
631
623
  const withUrn = sample.filter((i) => i != null && typeof i === "object" && typeof i.entityUrn === "string").length;
632
- if (withUrn >= sample.length * 0.5)
633
- return buildEntityIndex(arr);
624
+ if (withUrn >= sample.length * 0.5 && (!best || arr.length > best.length)) {
625
+ best = arr;
626
+ }
627
+ };
628
+ const obj = data;
629
+ for (const val of Object.values(obj)) {
630
+ if (Array.isArray(val)) {
631
+ check(val);
632
+ } else if (val != null && typeof val === "object" && !Array.isArray(val)) {
633
+ for (const nested of Object.values(val)) {
634
+ if (Array.isArray(nested))
635
+ check(nested);
636
+ }
637
+ }
634
638
  }
635
- return null;
639
+ return best ? buildEntityIndex(best) : null;
636
640
  }
637
641
  function resolvePath(obj, path5, entityIndex) {
638
642
  if (!path5 || obj == null)
@@ -658,7 +662,7 @@ function resolvePath(obj, path5, entityIndex) {
658
662
  }
659
663
  const rec = cur;
660
664
  let val = rec[seg];
661
- if (val === undefined && entityIndex) {
665
+ if (val == null && entityIndex) {
662
666
  const ref = rec[`*${seg}`];
663
667
  if (typeof ref === "string") {
664
668
  val = entityIndex.get(ref);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Reverse-engineer any website into reusable API skills. npm CLI + local engine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -118,28 +118,38 @@ function buildEntityIndex(items: unknown[]): Map<string, unknown> {
118
118
  return index;
119
119
  }
120
120
 
121
- /** Detect if an object contains a normalized entity array and build the index. */
121
+ /** Detect if an object contains a normalized entity array and build the index.
122
+ * Searches all top-level and one-level-nested arrays for entityUrn-keyed items,
123
+ * picking the largest qualifying array. Works for any normalized API shape. */
122
124
  function detectEntityIndex(data: unknown): Map<string, unknown> | null {
123
125
  if (data == null || typeof data !== "object") return null;
124
- const obj = data as Record<string, unknown>;
125
126
 
126
- // Check common locations: { included: [...] }, { data: { included: [...] } }
127
- const candidates: unknown[][] = [];
128
- if (Array.isArray(obj.included)) candidates.push(obj.included);
129
- if (obj.data && typeof obj.data === "object") {
130
- const d = obj.data as Record<string, unknown>;
131
- if (Array.isArray(d.included)) candidates.push(d.included);
132
- }
127
+ let best: unknown[] | null = null;
133
128
 
134
- for (const arr of candidates) {
135
- if (arr.length < 2) continue;
136
- const sample = arr.slice(0, 5);
129
+ const check = (arr: unknown[]) => {
130
+ if (arr.length < 2) return;
131
+ const sample = arr.slice(0, 10);
137
132
  const withUrn = sample.filter(
138
133
  (i) => i != null && typeof i === "object" && typeof (i as Record<string, unknown>).entityUrn === "string"
139
134
  ).length;
140
- if (withUrn >= sample.length * 0.5) return buildEntityIndex(arr);
135
+ if (withUrn >= sample.length * 0.5 && (!best || arr.length > best.length)) {
136
+ best = arr;
137
+ }
138
+ };
139
+
140
+ const obj = data as Record<string, unknown>;
141
+ for (const val of Object.values(obj)) {
142
+ if (Array.isArray(val)) {
143
+ check(val);
144
+ } else if (val != null && typeof val === "object" && !Array.isArray(val)) {
145
+ // One level deep: { data: { included: [...] } }, { response: { entities: [...] } }, etc.
146
+ for (const nested of Object.values(val as Record<string, unknown>)) {
147
+ if (Array.isArray(nested)) check(nested);
148
+ }
149
+ }
141
150
  }
142
- return null;
151
+
152
+ return best ? buildEntityIndex(best) : null;
143
153
  }
144
154
 
145
155
  /** Resolve a dot-path like "data.items[].name" against an object.
@@ -165,8 +175,10 @@ function resolvePath(obj: unknown, path: string, entityIndex?: Map<string, unkno
165
175
  const rec = cur as Record<string, unknown>;
166
176
  let val = rec[seg];
167
177
 
168
- // URN reference resolution: if direct lookup fails, check for "*key" reference
169
- if (val === undefined && entityIndex) {
178
+ // URN reference resolution: if direct lookup fails (or is null), check for "*key" reference.
179
+ // Normalized APIs (LinkedIn Voyager, Facebook Graph) set inline fields to null when
180
+ // the value is stored as a URN reference: e.g. socialDetail: null + *socialDetail: "urn:li:..."
181
+ if (val == null && entityIndex) {
170
182
  const ref = rec[`*${seg}`];
171
183
  if (typeof ref === "string") {
172
184
  val = entityIndex.get(ref);